Chapter 9: Registries & Distribution
Topic 56

Multi-Arch Images with buildx

BuildMulti-arch

driftwood/web has to run on the engineers' arm64 laptops and on amd64 — or Graviton arm64 — production hosts, and one image name should serve the right binary to each without anyone choosing a tag. docker buildx build --platform linux/amd64,linux/arm64 --push builds one image per architecture and publishes them under a single tag as a manifest list: the image index from Chapter 2 topic 08, now produced rather than just read. A host pulling driftwood/web:1.4.0 gets the manifest matching its own architecture automatically.

That single command hides three things worth pulling apart: what the manifest list does on the consumer side, what buildx and its builders do on the producer side, and why --push is not optional. Get those straight and multi-arch stops being the thing that surprises you when production hosts reject the image an arm64 laptop built.

One Tag, Many Architectures

A multi-arch build produces a manifest list under driftwood/web:1.4.0 that maps linux/amd64 and linux/arm64 each to their own per-arch image. This is the consumer side of Chapter 2's manifest list: the daemon on each host reads the list, finds the entry for its own platform, and pulls only that manifest and its layers. The arm64 laptop never downloads the amd64 image and vice versa, yet both used the same name.

One tag, two architectures, the right image to each host
driftwood/web:1.4.0
A manifest list — not an image, but an index pointing at one per-arch image for linux/amd64 and one for linux/arm64.
arm64 laptop · amd64 host
Each daemon reads the list, selects the entry matching its own platform, and pulls only that image. Same tag, right binary.

buildx and BuildKit Builders

docker buildx drives BuildKit and manages builder instances. The catch is that the default builder targets only the host's architecture — on an amd64 machine it can build amd64 and nothing else. Multi-arch needs a builder backed by either emulation or multiple native nodes, which you create once with docker buildx create and then reuse.

Creating a multi-arch builder and building both arches in one push
$ docker buildx create --name multi --driver docker-container --use
$ docker buildx inspect --bootstrap          # confirms which platforms it can target

$ docker buildx build \
    --platform linux/amd64,linux/arm64 \
    -t registry.driftwood.example/driftwood/web:1.4.0 \
    --push .
# builds one image per arch, assembles the manifest list, writes it to the registry

The docker-container driver is what makes multiple platforms possible — the default docker driver cannot. After the build, there is no per-arch image sitting in docker images to push afterward; buildx wrote the whole index to the registry as part of the build.

QEMU Emulation vs Native Builders

The simplest path registers QEMU so one machine can build foreign architectures in emulation — correct, but slow. For compile-heavy stages, an emulated foreign-arch build can run many times slower than native, sometimes turning a one-minute build into many minutes. The faster path attaches native arm64 and amd64 build nodes so each architecture builds on real hardware. Use emulation for the occasional local cross-build; back CI with native nodes when build time is on the critical path.

Build-and-Push as One Step

--push is required for a multi-arch build because the classic local image store has no representation for a manifest list. buildx assembles the per-arch images and the index and writes them straight to the registry — there is no intermediate docker images entry you could docker push separately. Leave --push off and the build either errors or, with --load, loads exactly one architecture, defeating the point. (The newer containerd image store — default on fresh Docker Engine 29.0+ installs — can hold a multi-platform index locally, so there --load keeps the whole thing; --push stays the portable choice that works on either store.)

This is the mental adjustment from single-arch builds. With one architecture you build, see it locally, then push. With multi-arch the build is the push: the registry is the only place a manifest list can exist, so producing one and publishing it are the same step.

Why arm64 Matters

Apple Silicon laptops are arm64, and AWS Graviton and other arm64 server fleets cut cost per core meaningfully. Shipping only amd64 forces emulation on every dev machine — slow, and occasionally subtly wrong in ways that waste an afternoon — and locks out arm64 production entirely. Building both architectures is the price of running everywhere Driftwood actually runs, and with buildx it is one flag, not a second pipeline.

Common Mistakes
  • Building on an arm64 laptop with a plain docker build and pushing it, then watching amd64 production hosts fail with exec format error — the image is arm64-only and the production kernel cannot run an arm64 binary; multi-arch has to be explicit, it is never inferred.
  • Expecting docker buildx build --platform linux/amd64,linux/arm64 to leave a usable image locally without --push — a manifest list has no single-arch local form, so the build either errors or --load brings back only one arch; multi-arch goes straight to a registry.
  • Relying on QEMU emulation for compile-heavy builds and blaming CI for being slow — emulated foreign-arch builds can run many times slower than native, and the fix is a native arm64 builder, not a faster runner.
  • Assuming a base image supports every target architecture — if FROM resolves to an amd64-only manifest, the arm64 build fails or silently emulates; the base's manifest list has to cover the platforms you target.
Best Practices
  • Build driftwood/web for linux/amd64,linux/arm64 in one docker buildx build --push so a single tag serves the right binary to laptops and production without anyone choosing an arch.
  • Back CI's buildx with native arm64 and amd64 builders rather than QEMU when build time matters, reserving emulation for occasional local cross-builds.
  • Pin a base image whose manifest list covers every target architecture, and verify the published tag is a manifest list with docker buildx imagetools inspect before relying on it cross-arch.
  • Push multi-arch images straight to the registry — a manifest list is a registry artifact, so make --push part of the build step rather than a separate push.
Comparable tools buildx · BuildKit the Docker-native multi-arch path Buildah builds multi-arch manifests with buildah manifest skopeo · docker buildx imagetools inspect and copy manifest lists QEMU binfmt_misc provides the cross-arch emulation underneath

Knowledge Check

A host pulls driftwood/web:1.4.0, a multi-arch tag. How does it get the right binary?

  • The tag is a manifest list, and the daemon reads it and pulls only the per-arch image matching its own platform
  • The image is a single fat universal binary that contains every architecture and runs on any one of them at once
  • The host always downloads the amd64 image and then emulates it on arm64 hardware transparently at runtime
  • You must append the target architecture to the tag yourself, like :1.4.0-arm64, every time you pull the image

Why is --push required for a multi-arch buildx build?

  • A manifest list has no single-arch local representation, so buildx writes it directly to the registry as part of the build
  • The registry must cryptographically sign the manifest list, and that signing step can only happen during a push to it
  • Without --push the per-arch layers are left fully uncompressed and end up too large to store in the local image cache
  • The local store cannot hold any built image, so every build must push

What is the trade-off between QEMU emulation and native builders for multi-arch?

  • QEMU needs no extra hardware but emulated foreign-arch builds are many times slower; native nodes are fast but you must provision them
  • QEMU produces a fundamentally different emulated image that will not run on the real target hardware afterward at all
  • Native builders are slower but cheaper to run, while QEMU is the faster path but needs paid licensing per architecture
  • Only native builders can ever produce a real manifest list, whereas QEMU emulation can only ever emit single-arch images that no host will select

Why does arm64 support matter for both development and production?

  • Apple Silicon laptops are arm64 and arm64 server fleets cut cost per core, so amd64-only forces emulation on dev and locks out arm64 prod
  • arm64 images are inherently more secure than their amd64 counterparts and therefore pass vulnerability scanning more easily
  • Supporting arm64 requires standing up and maintaining a completely separate parallel build pipeline alongside the amd64 one
  • arm64 images are exempt from the anonymous registry pull rate limits that amd64 images routinely run into on Docker Hub behind a shared egress IP

You got correct