Skip to content
docktionary

Docker

Buildx

Everything past the basic Dockerfile instructions — building for other architectures, keeping secrets out of layers, sharing cache across CI runs — lives in Buildx, the build frontend docker build has used by default since Docker 23.

What Buildx actually is

docker build and docker buildx build are, by default, the same thing — Buildx is the BuildKit-backed builder that's been wired into docker build since Docker 23. Everything on this page works with either command; buildx is spelled out explicitly here mostly because the features themselves (multi-platform, bake, custom builders) are commonly introduced that way. The legacy builder it replaced didn't support any of this — no parallel stage builds, no cache mounts, no build secrets, no cross-platform output.

Multi-platform builds

--platform builds one image manifest that covers several CPU architectures at once, so the same tag pulls the right variant automatically on an Intel laptop, an Apple Silicon laptop, or an Arm-based cloud instance.

terminal
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t docktionary/app:1.0 \
  --push .

--push is required here, not optional — a multi-platform build produces more than one image, and only a registry (not the local daemon's single-architecture image store) can hold a manifest list that points at several of them. The default builder cross-builds other architectures through QEMU emulation, which works but is noticeably slower than a native build; for frequent multi-arch builds, a dedicated native Arm runner (or a builder instance with native nodes — see below) is the faster option.

Build secrets

An ARG or ENV holding a credential ends up baked into a layer, readable by anyone with the image — including after you remove it in a later instruction, since the earlier layer still has it. --secret mounts a value into the build only for the instruction that asks for it, and it's never written to a layer at all.

Dockerfile
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
terminal
docker buildx build --secret id=npm_token,src=$HOME/.npm_token -t docktionary/app .

The # syntax=docker/dockerfile:1 line matters — it opts into the newer Dockerfile frontend that understands --mount, which isn't part of the stable Dockerfile syntax Docker shipped before BuildKit.

Cache mounts

RUN --mount=type=cache gives an instruction a persistent directory that survives between builds but, like a secret mount, never becomes part of a layer. It's built for package manager caches: instead of re-downloading every dependency on every build (or baking a huge cache into a layer that bloats the image), the cache lives alongside the build and just gets faster over time.

dockerfile
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

The same pattern works for any package manager with a cache directory — --mount=type=cache,target=/root/.cache/pip for pip, target=/go/pkg/mod for Go modules, target=/var/cache/apt for apt (paired with rm -f /etc/apt/apt.conf.d/docker-clean so apt doesn't clean the cache it was just given).

Sharing cache across machines and CI runs

The build cache this site's Dockerfile pages rely on (reusing a layer if nothing upstream changed) normally lives only on the machine that built it. A CI runner that starts from a clean checkout every time gets none of that benefit by default. --cache-to/--cache-from export and import that cache somewhere shared.

terminal
# Share cache via a registry -- works from any CI runner
docker buildx build \
  --cache-to type=registry,ref=docktionary/app:cache,mode=max \
  --cache-from type=registry,ref=docktionary/app:cache \
  -t docktionary/app:1.0 --push .

# GitHub Actions' own cache, no registry needed
docker buildx build \
  --cache-to type=gha,mode=max \
  --cache-from type=gha \
  -t docktionary/app:1.0 .

mode=max caches every layer, including ones from intermediate stages of a multi-stage build that the final image never ships — worth the extra cache storage, since those are usually the slowest layers to rebuild (dependency installs, compile steps).

Builder instances

The default builder runs as a process on the local Docker daemon, which is exactly why it can't produce a native multi-platform image on its own — it only has your machine's one architecture. A docker-container builder runs inside its own container (or, for real native multi-arch speed, as several nodes on different-architecture machines) and is what actually backs most multi-platform builds.

terminal
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap
docker buildx ls
docker buildx build --builder multiarch --platform linux/amd64,linux/arm64 -t app --push .

Bake: building several targets at once

docker buildx bake reads a declarative file describing multiple build targets — different Dockerfiles, platforms, or build args — and builds them together, instead of a shell script looping over separate docker build calls.

docker-bake.hcl
group "default" {
  targets = ["app", "worker"]
}

target "app" {
  dockerfile = "Dockerfile"
  tags       = ["docktionary/app:latest"]
  platforms  = ["linux/amd64", "linux/arm64"]
}

target "worker" {
  dockerfile = "Dockerfile.worker"
  tags       = ["docktionary/worker:latest"]
}
terminal
docker buildx bake

Independent targets build in parallel, same as independent stages within a single Dockerfile — bake is really the multi-Dockerfile version of the dependency-graph building multi-stage builds already get within one file.

Attestations: provenance and SBOM

--provenance and --sbom attach signed metadata to the image describing how it was built and exactly what software it contains — increasingly what registries and scanners expect for any image claiming to be production-ready, as part of verifying a software supply chain rather than trusting an image blindly.

terminal
docker buildx build --provenance=true --sbom=true -t docktionary/app --push .
docker buildx imagetools inspect docktionary/app --format '{{ json .SBOM }}'

Attaching these attestations is only half the story -- see signing & verification for actually checking them (and the image itself) against an identity you trust, rather than just assuming they're there.