Skip to content
Blog

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:

Featuredockerdocker-containercloudkubernetesremote
Automatically load imageyesnoconditionalnono
Cache exportyes (not all options)yesyesyesyes
Tarball outputnoyesyesyesyes
Multi-arch imagesnoyesyesyesyes
BuildKit configurationnoyesmanaged by Dockeryesmanaged externally

Two consequences:

  1. The default docker driver loads into the local image store automatically but cannot build multi-arch images and supports only a subset of cache export options.
  2. The docker-container driver gets every feature but loads nothing by default. If you do not pass an output, the result stays in the build cache - which is why docker images looks 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 FROM instructions without their own --platform pull for the target platform, and TARGETPLATFORM is available as a build argument inside the Dockerfile.
  • RUN steps need runtime support for that platform. Buildx picks up binfmt_misc launchers automatically, and Docker Desktop ships with binfmt_misc configured for arm64 and arm. Verify with docker buildx inspect --bootstrap.
  • On a clean Linux setup you can only execute RUN for 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:

ScenarioCommand
Single platform, local iteration, image in docker images right awaydocker build -t app:dev .
Need cache export/import, extra outputs, --check, richer progress controldocker buildx build with the docker-container driver
Multi-arch manifest listdocker buildx build --platform ... --push
Several related targets sharing args/tagsdocker buildx bake
Windows container imagelegacy 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 version and docker buildx ls run 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: docker for local simplicity, docker-container for 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-to for the backend it uses (gha, registry, local), with mode=max chosen knowingly.
  • --progress=plain is set in CI logs and --metadata-file captures 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