skip to content

What does `docker buildx build --push` do that `docker build` followed by `docker push` does not?

level: juniorimportance: must knowfreq 72%

answer

  1. Ask where the finished image landed
  2. Buildx builders have their own storage
  3. The flag is an exporter shorthand
  4. type=registry versus type=docker
  5. Log in before the build, not after

basics

~20 s

--push is shorthand for --output type=registry: buildx streams the finished layers from the builder straight to the registry. On a docker-container builder the image never enters the local image store, so a separate docker push would find nothing to push.

solid answer

~50 s

In buildx, where a build result lands is an explicit choice called an exporter, selected with `--output`. `--push` is shorthand for `--output type=registry` and `--load` is shorthand for `--output type=docker`. That matters because a CI job normally builds on a builder created with the `docker-container` driver, which is a separate BuildKit container with its own content store — nothing is written into the engine's image store automatically. Build with no output flag and buildx warns that the result will only remain in the build cache; `docker images` shows nothing and a follow-up `docker push` fails because no local image carries that tag. `--push` also means the upload happens at the end of the same command, so the runner must already be authenticated to the registry non-interactively before the build starts, or you burn the whole build and fail at the last step.

code

bash · 3 lines
bash
echo "$REGISTRY_TOKEN" | docker login registry.example.com -u ci-bot --password-stdin
docker buildx create --name ci --driver docker-container --use --bootstrap
docker buildx build --push -t registry.example.com/geo/tile-api:build-4712 .

go deeper

for a junior

Recall that --push is an output/exporter shorthand, not a second command, and that a buildx build can succeed while leaving nothing in docker images. Knowing --load as its counterpart is the other half of the answer.

for a middle

Explain the mechanism: exporters chosen with --output, type=registry versus type=docker, and why a builder that is a separate BuildKit container has no reason to touch the engine's image store. Be able to read the no-output warning correctly.

for a senior

Show the operational consequence — the upload runs at the end of a long build, so credentials must be in place before it starts, and unchanged blobs are skipped by digest. Talk about not materialising layers twice on a runner's disk.

for a principal

Own the policy: which jobs are allowed to publish at all, whether builds and publishes are the same command or deliberately separated so an artifact can be tested before it becomes visible, and what that choice costs in runner time and in reproducibility.

### Where a build result actually goes Classic `docker build` hands the build to the Docker daemon and, on success, writes the image into the daemon's **local image store** under whatever tags you passed with `-t`. From there `docker images` lists it, `docker run` can start it, and `docker push` is a second, separate command that reads that stored image and uploads it. Two steps, two commands, one place in the middle where the image sits. Buildx keeps the command shape but changes the model underneath. A buildx build runs on a named **builder instance**, and the destination of the result is an explicit choice called an **exporter**, selected with `--output`. `--push` is shorthand for `--output type=registry`. `--load` is shorthand for `--output type=docker`, which imports the result into the local image store. Other exporters write a directory of files (`type=local`), a tarball (`type=tar`), an OCI layout (`type=oci`), or nothing but cache (`type=cacheonly`). ### Why the difference bites specifically in CI The capability you get for free depends on the builder's **driver**. The default builder uses the `docker` driver — BuildKit embedded in the daemon — and it loads results into the local image store automatically, which is why `docker build` on a laptop feels like it always did. A CI job normally creates its own builder with the `docker-container` driver, because that driver can do things the embedded one cannot. That builder is a separate BuildKit container with its own content store, and it does **not** write into the engine's image store unless you ask. So on a CI runner, a plain `docker buildx build -t app:1 .` with no output flag produces a warning that no output was specified and that the build result will only remain in the build cache. `docker images` shows nothing. The next line of the job, `docker push app:1`, then fails saying no local image exists with that tag — and the failure is confusing precisely because the build printed a successful, fully green log a second earlier. ### The push is part of the build command With `--push`, BuildKit uploads each layer as the build finishes it, straight from the builder to the registry, and then writes the manifest under every tag you passed with `-t` (you can repeat `-t`). Practical consequences: - **No double storage on the runner.** Layers are streamed out instead of being materialised into the local image store and then read back out again for an upload. - **Blobs the registry already has are skipped.** The push negotiates by digest, so an unchanged base or unchanged dependency layer costs a HEAD request rather than an upload. - **It is the only exporter that can publish a manifest list.** A result built for more than one platform cannot be represented in the local image store the way a single image can, so the registry is the destination. - **Authentication must already be in place.** The upload happens at the *end* of a build that may run for minutes. Buildx takes the credentials from the CLI's config file and passes them to the builder over the build session, so the job has to authenticate non-interactively first — a runner has no TTY, so an interactive password prompt would simply hang the job until it times out. Log in as an early step, not as a step before the push, because there is no separate push step to precede. ### A worked example A geospatial tile server ships as a Node.js API on an alpine base. Its CI job logs in, creates a `docker-container` builder, and runs one build command with `--push -t registry.example.com/geo/tile-api:build-4712`. The 6 m 41 s build ends with an upload of the four layers that changed; the other layers are already in the registry and are skipped. Nothing is left in the runner's image store, which is fine, because the runner is discarded moments later. If that same job needs the image locally as well — say a smoke test runs it before publishing — the honest shape is to `--load` it, test it, and push afterwards, or to run the test as part of the build. What does not work is assuming that a buildx build has quietly populated `docker images` for you. ### The short version `docker build` + `docker push` is build-into-a-store, then upload-from-the-store. `docker buildx build --push` is build-and-upload as one operation, with no local copy in between, on a builder that would not have made a local copy anyway.

  • What would you use instead of `--push` if the job must run the image before publishing it?
    `--load`, which is shorthand for `--output type=docker` and imports the result into the runner's local image store so `docker run` can start it. It costs an extra materialisation of the layers on the runner's disk, and it cannot import a multi-platform result, so a job that both tests and publishes typically loads a single-platform image for the smoke test and pushes in a separate build, or runs the test inside the build itself.
  • The job builds for six minutes and then fails on the push with an authorization error. What went wrong and how do you avoid it?
    The runner was not authenticated to the target registry when the exporter ran. Because `--push` uploads at the end of the same command, missing credentials cost the entire build before surfacing. Fix it by making the non-interactive login one of the job's first steps, and by failing fast — a cheap credential check early is worth six minutes of runner time on every broken build.
  • Can you push several tags from one buildx build?
    Yes — repeat `-t`. All tags reference the same manifest and the same layer blobs, so the extra tags cost only additional manifest writes, not another upload of the content. That is why publishing an immutable build-specific tag alongside a moving one is essentially free at push time.

Classic build is printing a document to your desk and mailing it later; --push is sending it straight to the print shop's outbox. Look on the desk afterwards and there is nothing there — which is the point, not a bug.

saying these in an interview costs you the question

  • Assumes the image always appears in docker images after a buildx build
  • Describes --push as just running docker push afterwards
  • Adds a separate docker push step and cannot explain the failure
  • Thinks buildx authenticates to the registry on its own
  • Confuses --load with --push, or thinks they are the same exporter
  • Believes an output flag is optional because the build exits zero

context