skip to content

A `docker buildx build` on a docker-container builder succeeds, yet `docker images` lists nothing new. Why, and how do you get the image out?

level: seniorimportance: should knowfreq 45%

answer

  1. Where does BuildKit itself run?
  2. The builder is not the engine
  3. Nothing is exported unless asked
  4. --load, --push, --output
  5. docker build routes through buildx now

basics

~20 s

The docker-container buildx driver runs BuildKit outside the engine's image store and exports nothing by default, so the result stays in the builder. Add --load to import it into the local engine, or --push to publish it.

solid answer

~40 s

A buildx **driver** decides where BuildKit runs. On the default `docker` driver BuildKit is inside the engine, so a result lands straight in the engine's image store and `docker images` shows it. On a `docker-container` builder, BuildKit runs in its own container and there is **no default exporter**: the result stays as content in the builder, and the CLI warns that it will only remain in the build cache. You get it out with an exporter — `--load` (shorthand for `--output type=docker`) imports it into the local engine, `--push` (shorthand for `--output type=image,push=true`) sends it to the registry named by the tag, and full `--output` forms such as `type=local,dest=./dist` write files instead of an image. Since Engine 23.0 a plain `docker build` also routes through buildx, so this bites builds that never mention buildx.

code

bash · 10 lines
bash
docker buildx create --name ci --driver docker-container --use

# result stays in the builder - docker images shows nothing
docker buildx build -t fraud-scoring:2.9.4 .

# import into the local engine image store
docker buildx build --load -t fraud-scoring:2.9.4 .

# publish straight to the registry, no local copy
docker buildx build --push -t registry.example.com/fraud-scoring:2.9.4 .

go deeper

for a junior

Know that --load is what brings a buildx result into your local Docker so docker run can start it, and that --push sends it to a registry instead. Check docker buildx ls when an image seems to be missing.

for a middle

Explain the driver-to-export relationship: with the docker driver BuildKit is the engine, so results land locally; with docker-container it is not, so an exporter is required. Know --load and --push as shorthands for --output forms.

for a senior

Diagnose this quickly in a pipeline — identify the active builder, the requested exporter, and where the artefact actually is — and design steps that pull by tag or digest rather than assuming a shared local image store.

for a principal

Set the convention for the organisation: which driver runs builds in which environment, whether CI publishes or hands off files, and how you keep developer and CI builds from behaving differently in ways that only surface at release time.

### buildx builders are not all the same engine `docker buildx` is the CLI front end for BuildKit, and every build runs on a **builder instance**. Each instance is created with a **buildx driver**, and the driver decides *where BuildKit itself runs* — which in turn decides where a finished build result can land. `docker buildx ls` prints the instances, their driver, and their status. Three drivers matter day to day: * **`docker`** — the BuildKit built into the Docker Engine you are talking to. You get it for free; there is nothing to create. Because BuildKit is inside the engine, a successful build lands directly in that engine's image store, so `docker images` shows it and `docker run` can start it. In exchange you are limited to what the engine's embedded BuildKit exposes, and you cannot configure it independently. * **`docker-container`** — buildx starts a BuildKit container on the engine and drives it. This is the driver you create with `docker buildx create --driver docker-container`. It runs a current BuildKit independent of the engine's version and gives you full control over its configuration. Crucially, it lives in a container: its results are **outside** the engine's image store. * **`remote`** — buildx connects to a `buildkitd` that someone else already runs, over a URL you provide. Nothing is started locally; the builder is shared infrastructure with its own lifecycle, capacity and access control. ### Why the image "disappears" This is the part that bites teams the first time they move CI onto a shared builder. Consider a fraud-scoring endpoint whose pipeline does `docker buildx build -t fraud-scoring:2.9.4 .` on a `docker-container` builder and then a smoke test that runs `docker run fraud-scoring:2.9.4`. The build reports success. The smoke test fails with an unknown image. Nothing went wrong. With the `docker-container` driver there is **no default export**: the build result exists as content inside the builder container and is not handed to the engine. The CLI even warns that no output was specified and the result will remain only in the build cache. The `docker` driver hides this distinction, because there the builder *is* the engine, so "build" and "have the image locally" look like the same act. ### Exporters: how a result leaves the builder An exporter turns the solved result into something outside BuildKit. The two shorthands cover most use: * `--load` is shorthand for `--output type=docker` — export the image and import it into the local engine's image store, so `docker images` and `docker run` see it. * `--push` is shorthand for `--output type=image,push=true` — export as an image and push it to the registry named by the tag, never touching the local image store. `--output` in full form reaches further. `type=registry` pushes; `type=oci` and `type=docker` with a `dest=` write a tarball; `type=local,dest=./out` writes the **target stage's filesystem** into a host directory, producing no image at all. That last one is genuinely useful: for an nginx-fronted static bundle you can have the build produce the compiled assets on disk for a CDN upload, with the same Dockerfile that produces the image. Two exporters can be requested in one build, which is how a pipeline pushes to a registry and keeps a local copy for its smoke test in a single invocation. ### Choosing, and the failure modes The fast triage when "the build worked but the image is missing": 1. `docker buildx ls` — which builder ran it, on which driver? 2. Was any exporter requested? No `--load`, `--push` or `--output` on a non-`docker` driver means the result stayed in the builder. 3. If `--push` was used, the image is in the registry and never was local; pull it or add `--load`. The corresponding design rules: CI that publishes should `--push` and then have downstream steps pull by tag or digest, rather than assuming a local image store; CI that only tests should `--load` explicitly; and a developer loop that never leaves the laptop is usually happiest on the plain `docker` driver, where the default behaviour matches the intuition that building gives you a local image. ### The subtlety about `docker build` Since Docker Engine 23.0, `docker build` routes to `docker buildx build` when the buildx plugin is installed, using whichever builder is currently selected (`docker buildx use`). So a plain `docker build` can silently be running on a `docker-container` builder someone selected weeks ago — and then it, too, produces no local image. When the symptom appears without anyone having typed `buildx`, that is usually why.

  • When would you deliberately choose a docker-container builder over the default docker driver?
    When you need a BuildKit whose version and configuration you control independently of the engine, or features the engine's embedded builder does not expose. It is also the honest choice in CI, where the build should not depend on whatever the host engine happens to ship. The cost is the explicit export step and a container to manage; a pure laptop loop is usually simpler on the plain `docker` driver.
  • What does the `remote` buildx driver change compared with `docker-container`?
    Nothing is started locally: buildx connects to a `buildkitd` that already exists at a URL you configure, so the builder is shared infrastructure with its own capacity, lifecycle and access control. That gives ephemeral CI runners a builder that outlives them, but it makes the builder a shared failure domain and a place where several teams' build inputs meet, which needs deliberate access control.
  • What does `--output type=local,dest=./dist` produce, and when is that useful?
    No image at all — BuildKit writes the target stage's filesystem into that host directory. It is the right exporter when the build's real product is files rather than a container: compiled binaries, generated clients, or a static asset bundle destined for a CDN. You get the same Dockerfile, hermetic build environment and cache behaviour, with a directory as the artefact.
  • How do you push and still keep a local copy for a smoke test in one build?
    Request two exporters in the same invocation — for example `--push` together with `--load` — so the result is both published and imported into the local engine. The alternative is to push, then pull the tag back, which costs a registry round trip but is what you want anyway if the smoke test is meant to prove the published artefact works.

The default docker driver is a printer wired to your desk — press print and the page is in your hand. A docker-container builder is a print shop across town: the job completed, but nothing arrives until you say deliver here or ship it to the customer.

saying these in an interview costs you the question

  • Assumes every builder writes into the local image store
  • Thinks the build silently failed
  • Believes --push also leaves a local copy
  • Cannot name any driver other than the default
  • Reaches for docker save when --load exists
  • Ignores that plain docker build now uses buildx

context