Docker Buildx: The Next-Generation Build CLI
A practical buildx tutorial: verify the plugin, understand builders and drivers, master --load/--push/--platform, wire up cache export and import, and know when plain docker build is enough.
Published on • October 17, 2026
AI Assistant

You run docker buildx build -t app:1.0 ., the build reports success, and docker images shows nothing. Or you add --cache-to type=gha and get an error, or you ask for --platform linux/amd64,linux/arm64 and the local store rejects the result. None of these are Dockerfile bugs - they are consequences of buildx’s builder and output model, which behaves differently from the single-queue docker build you learned first.
Buildx is now the front end for every build: the reference for docker buildx build lists docker build, docker builder build and docker image build as aliases, and Docker documents BuildKit as the default builder for Docker Desktop and Docker Engine (Windows containers still use the legacy builder). This guide covers what you actually need: verifying the plugin, choosing a builder and driver, the output rules that trip everyone up, multi-platform builds, cache export/import, Bake, and the cases where plain docker build is still the right answer.
Verify the install
Buildx ships as a Docker CLI plugin. Docker Desktop bundles it; on Linux you install it into ~/.docker/cli-plugins/ (or /usr/libexec/docker/cli-plugins/). Check it:
$ docker version --format '{{.Client.Version}}'
$ docker buildx version
github.com/docker/buildx v0.29.0 <hash>
Then look at what builders exist:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default docker
default default running v0.20.x linux/amd64, linux/arm64, ...
docker buildx is a small umbrella command - ls, inspect, use, create, rm, stop, prune, du, plus build, bake, imagetools, history, policy, replay - with a global --builder flag (also BUILDX_BUILDER) to override the configured builder per invocation, and -D for debug logging.
Builders and drivers
A builder is an instance; its driver decides where BuildKit runs. The documented drivers are:
docker- BuildKit library bundled into the Docker daemon (the default).docker-container- a dedicated BuildKit container managed through Docker.cloud- a managed builder in Docker Build Cloud.kubernetes- BuildKit pods in a Kubernetes cluster.remote- a manually managed BuildKit daemon you point at directly.
Docker’s driver comparison table is worth memorizing, because it explains almost every “why didn’t my build do X” moment:
| Feature | docker | docker-container | cloud | kubernetes | remote |
|---|---|---|---|---|---|
| Automatically load image | yes | no | conditional | no | no |
| Cache export | yes (not all options) | yes | yes | yes | yes |
| Tarball output | no | yes | yes | yes | yes |
| Multi-arch images | no | yes | yes | yes | yes |
| BuildKit configuration | no | yes | managed by Docker | yes | managed externally |
Two consequences:
- The default
dockerdriver loads into the local image store automatically but cannot build multi-arch images and supports only a subset of cache export options. - The
docker-containerdriver gets every feature but loads nothing by default. If you do not pass an output, the result stays in the build cache - which is whydocker imageslooks empty after a “successful” build.
Creating and switching builders:
$ docker buildx create --name mybuilder --driver docker-container --use
$ docker buildx use mybuilder
$ docker buildx ls
$ docker buildx inspect --bootstrap # starts the builder and shows platforms
inspect --bootstrap is also how you check which runtime platforms your builder can actually execute RUN instructions for (see multi-platform below). If you like the container driver but miss auto-loading, create with --driver-opt default-load=true.
Build basics and the output model
The basic build is unchanged:
$ docker buildx build -t registry.example.com/app:1.0 .
What changed is that a build has an explicit export step. The --output / -o flag selects it, with type=local (files to a directory), tar, oci, docker, image and registry as the supported types. The reference notes that the docker driver supports only the local, tarball and image exporters, while docker-container supports all of them.
The two shorthands you will use daily:
# --load = --output=type=docker
$ docker buildx build --load -t app:dev .
# --push = --output=type=registry,unpack=false
$ docker buildx build --push -t registry.example.com/app:1.0 .
And the rule that saves an afternoon: on any driver that does not auto-load, no output flag means no visible image. Either --load it, --push it, or export it with -o type=oci,dest=app.tar.
Useful companions:
$ docker buildx build --load \
--tag app:1.4.2 \
--build-arg NODE_ENV=production \
--target release \
--file Dockerfile.prod \
--progress=plain \
--metadata-file metadata.json \
.
--metadata-file writes the image digest and build reference as JSON - the reliable way to pipe a built digest into a later sign or scan step. --progress accepts auto (TTY detection), tty, plain, quiet and rawjson, and can be set globally with BUILDKIT_PROGRESS. For CI logs, plain is the one to pick.
Buildx also adds inspection methods you should know about:
$ docker buildx build --call=targets . # list stages and the default target
$ docker buildx build --call=outline . # show build args and their defaults
$ docker buildx build -q --check . # evaluate build checks without building
--check runs Dockerfile lint-style validations (for example an InvalidBaseImagePlatform warning) without executing the build - cheap to add as a CI step before the real build.
Multi-platform builds
--platform takes os/arch[/variant], and with the docker-container driver it accepts a comma-separated list which becomes one manifest list:
$ docker buildx build --push \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/app:1.0 .
Details from the reference:
- All
FROMinstructions without their own--platformpull for the target platform, andTARGETPLATFORMis available as a build argument inside the Dockerfile. RUNsteps need runtime support for that platform. Buildx picks upbinfmt_misclaunchers automatically, and Docker Desktop ships withbinfmt_miscconfigured forarm64andarm. Verify withdocker buildx inspect --bootstrap.- On a clean Linux setup you can only execute
RUNfor your own architecture until you install qemu/binfmt for the rest. - The default image store in Docker Engine cannot load multi-platform images. Either push the manifest list directly (as above) or enable the containerd image store.
A cross-build that must also end up locally for testing is best done as two steps: --load for your own platform, --push for the full set.
Cache export and import
The local cache of the active builder is only half the story; CI needs to move cache between jobs. That is --cache-from and --cache-to:
$ docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
-t app:ci .
Supported backends for both directions: registry, local, gha, s3, azblob, plus inline for --cache-to (cache metadata written into the image config). If no type is given, the registry exporter is used with the reference you pass. A registry example and a local example:
$ docker buildx build --cache-from type=registry,ref=registry.example.com/app:cache .
$ docker buildx build --cache-to type=local,dest=./cache \
--cache-from type=local,src=./cache -t app:dev .
Constraints to plan around: the default docker driver does not support every cache export option (the driver table flags this explicitly), and inline cache only helps if you push that exact image. mode=max exports intermediate stages too - more coverage, more upload; the blog’s dependency-caching post goes into the GitHub Actions specifics.
Build contexts, secrets and SSH
Three buildx-native features that replace hacks:
# Extra named contexts usable as FROM name / COPY --from=name
$ docker buildx build --build-context project=../other-repo .
# Secrets mounted via RUN --mount=type=secret, never baked into a layer
$ docker buildx build --secret id=npmrc,src=$HOME/.npmrc .
# SSH agent socket for private dependency fetch
$ docker buildx build --ssh default .
--build-context accepts a local directory, a container image (docker-image://...), an OCI layout, a Git URL or an HTTP URL - handy for pinning a base image by digest while still writing a readable FROM.
Bake: build from a file
When a build has five tags, four platforms and shared build args, the command line stops being a record of what you did. docker buildx bake builds from a definition file instead:
# docker-bake.hcl
group "default" {
targets = ["db", "webapp-dev"]
}
target "db" {
dockerfile = "Dockerfile.db"
tags = ["docker.io/username/db"]
}
target "webapp-dev" {
context = "."
dockerfile = "Dockerfile"
tags = ["docker.io/username/webapp-dev"]
args = {
GO_VERSION = "1.22"
}
}
$ docker buildx bake # builds the default group
$ docker buildx bake webapp-dev
$ docker buildx bake -f bake.prod.hcl --push
-f / --file accepts HCL, JSON or a Compose file, and multiple files are merged; BUILDX_BAKE_FILE sets it via environment (ignored if -f is also present). Bake inherits most buildx build flags as overrides, so --platform, --push and --cache-to work unchanged, and variables let one file serve local and CI invocations.
When plain docker build is still fine
Buildx is the implementation behind docker build now, so this is not a purity question - it is about which flags you need. A quick decision table:
| Scenario | Command |
|---|---|
Single platform, local iteration, image in docker images right away | docker build -t app:dev . |
Need cache export/import, extra outputs, --check, richer progress control | docker buildx build with the docker-container driver |
| Multi-arch manifest list | docker buildx build --platform ... --push |
| Several related targets sharing args/tags | docker buildx bake |
| Windows container image | legacy builder path (BuildKit is not the default there) |
Staying on docker build is genuinely fine when the default docker driver’s limits do not bite: you are not exporting cache, you are not producing a manifest list, and the automatic load into the local image store is exactly what you want. The moment you need mode=max GHA cache, a registry export, or linux/arm64, move the same Dockerfile over to docker buildx build - the Dockerfile does not change, only the delivery does.
Checklist
-
docker buildx versionanddocker buildx lsrun on every machine and CI runner that builds images. - You know which builder is active (
docker buildx ls,docker buildx use), and CI creates a fresh one instead of relying on an interactive default. - The driver choice is deliberate:
dockerfor local simplicity,docker-containerfor cache export, multi-arch and configurability. - Every build specifies an output -
--load,--push, or an explicit-o- so results never silently vanish into the build cache. - Multi-arch builds are verified with
docker buildx inspect --bootstrap, and pushed (or exported) because the default image store cannot load manifest lists. - CI passes
--cache-from/--cache-tofor the backend it uses (gha,registry,local), withmode=maxchosen knowingly. -
--progress=plainis set in CI logs and--metadata-filecaptures digests for downstream sign/scan steps. - Repetitive multi-target builds have moved from shell scripts to a
docker-bake.hcl(or Compose) file.
Sources: docker buildx reference, docker buildx build reference, Build drivers, BuildKit, docker buildx bake reference, Docker build overview