buildx & Multi-Arch Images
Summary
docker buildxbuilds a single image reference that carries per-architecture variants behind one manifest list, soamd64andarm64nodes pull the right one automatically.- Insight: A multi-arch tag is not one binary; it is a manifest list that indexes several images by platform.
- Key Concepts: manifest list (image index),
--platform, buildx builder, QEMU emulation, native build nodes. - When to Use: Mixed clusters (Graviton plus x86), developers on Apple Silicon, and any image consumed on more than one CPU architecture.
- Limitations: Emulated cross-builds are slow; native multi-node builders are faster but need more infrastructure.
Recipe
Create a buildx builder, then build and push both platforms in one command.
docker buildx create --name multi --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/api:1.0 --push .The result is one tag whose manifest list points at an amd64 and an arm64 image. Each node pulls the variant matching its CPU.
Working Example
A build that produces both platforms and verifies the manifest.
# One-time: a builder that uses the docker-container driver
docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap
# Build and push a multi-arch image
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/api:1.0 \
--push .
# Confirm the manifest list has both platforms
docker buildx imagetools inspect registry.example.com/api:1.0imagetools inspect prints the index and each platform entry, so you can confirm linux/amd64 and linux/arm64 are both present.
For cross-compiled languages, use BuildKit's build args to target the requested platform natively instead of emulating it.
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -o /out/api ./cmd/api
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/api /api
USER nonroot
ENTRYPOINT ["/api"]Here the builder always runs on the native $BUILDPLATFORM and cross-compiles to $TARGETARCH, avoiding slow QEMU emulation entirely.
Deep Dive
Manifest lists
A multi-arch tag is an OCI image index. It lists child manifests, each tagged with os and architecture, so a client selects the matching image at pull time.
The default (docker) buildx driver cannot produce manifest lists. Multi-platform builds need the docker-container or a Kubernetes/remote driver.
Emulation versus native builds
With one build node, buildx emulates the foreign architecture using QEMU via binfmt_misc. It works for any Dockerfile but is slow for compile-heavy stages.
Two faster paths exist. Cross-compile with $BUILDPLATFORM and $TARGETARCH so the toolchain runs natively, or attach native builder nodes of each architecture so each platform builds on real hardware.
docker buildx create --name multi --node amd --platform linux/amd64
docker buildx create --name multi --append --node arm --platform linux/arm64Why arm64 matters
Apple Silicon laptops are arm64, and AWS Graviton nodes are arm64, so an amd64-only image either fails to run or runs under slow emulation on those hosts.
Publishing both variants means the same tag runs natively across developer machines and mixed-architecture Kubernetes node pools.
Digests, provenance, and pushing
The manifest list and each child image are content-addressed by digest. Pin deployments to the list digest, and the kubelet still resolves the correct per-architecture child at pull time.
buildx can also attach SBOM and provenance attestations with --provenance and --sbom, which land alongside the image index in the registry. These support supply-chain policies without changing how the image runs.
Because a manifest list cannot live in the single-image local store, multi-platform builds either push to a registry or export with --output type=oci,dest=out.tar. Plan your CI to push, then promote by digest.
Gotchas
Building with the default driver silently produces a single-arch image. Create and select a docker-container builder first.
--platform without --push cannot store a manifest list in the local Docker image store; multi-arch results must go to a registry (or --output). Push, or export an OCI layout.
QEMU cross-builds can hit subtle bugs in emulated toolchains. Prefer native cross-compilation for compiled languages.
Base images without an arm64 variant break the build for that platform. Check the base supports every target with imagetools inspect.
A Kubernetes nodeSelector or affinity that ignores architecture can schedule an amd64-only pod onto an arm64 node. Publish both arches or constrain scheduling with the kubernetes.io/arch label.
Alternatives
Building one architecture per pipeline and stitching a manifest with docker manifest create works without buildx, but it is more manual and error-prone.
CI matrix builds on native runners per architecture avoid emulation entirely and then merge into an index, which suits teams that already run heterogeneous CI fleets.
Buildpacks and some registry services can assemble multi-arch images too, trading Dockerfile control for a managed workflow.
FAQs
Why is my arm64 build so slow? It is likely QEMU emulation. Cross-compile with $TARGETARCH or add a native arm64 builder node.
Why does my multi-arch build fail to load locally? The local docker store holds single images, not manifest lists. Use --push to a registry or --output type=oci.
How do I confirm both platforms shipped? Run docker buildx imagetools inspect <ref> and check for linux/amd64 and linux/arm64 entries.
Do I need one builder or two nodes? One emulated builder works for any Dockerfile. Native cross-compilation or two nodes are faster for compiled code.
Will Kubernetes pick the right variant? Yes. The kubelet and containerd pull the manifest entry matching the node's architecture automatically.
Does a manifest list increase pull size? No. Each node pulls only its matching variant, not the whole index.
Can I add more platforms later? Yes. Rebuild the tag with the extra platform in --platform and push; the new index replaces the old one for that tag.
Do I still tag per architecture? Usually no. One multi-arch tag is simpler for consumers. Per-arch tags are only useful when you must pin a specific variant explicitly.
Related
- Multi-Stage Builds
- BuildKit & Cache Mounts
- How a Dockerfile Becomes an Image
- Distroless & Minimal Bases
- Dockerfiles Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).