DevOps · Docker

Docker Buildx Multi-Architecture Builds

Docker Buildx Multi-Architecture Builds is the work that turns a deploy into a system. The deployment is one moment; the system is the next 18 months of uptime, incidents, and.

John Kihiu12 min read

Buildx is Docker's extended build engine, built on BuildKit, and its most useful trick is building a single image that runs correctly on both x86-64 and ARM64 without owning a machine of each architecture. That matters more than it used to: Apple Silicon laptops, AWS Graviton instances, and Raspberry Pi deployments are all ARM64, while most CI runners and older cloud fleets are still x86-64. A multi-arch image is what lets `docker pull myapp:latest` just work regardless of which one pulled it.

How cross-architecture builds actually work

Buildx has two ways to produce a non-native architecture: QEMU emulation, or a native builder on that architecture. `docker buildx create --use` sets up a builder that, combined with the `docker/setup-qemu-action` (in GitHub Actions) or a local `binfmt_misc` registration, can emulate ARM64 instructions on an x86-64 host well enough to run the build steps. Emulation is slow — a `RUN npm install` or `RUN cargo build` step can take several times longer under QEMU than native — but it needs no extra infrastructure. The faster alternative is a native builder: point buildx at an actual ARM64 machine (a real Graviton instance, an Apple Silicon Mac, or a remote builder) and let each architecture build on hardware that matches it.

BASH
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/app:1.4.0 \
  --push .

The manifest list, not two images

A multi-arch build doesn't produce two separate tagged images — it produces one tag backed by a manifest list (an OCI image index) that points to an amd64 image and an arm64 image sharing the same tag. When a client pulls `app:1.4.0`, the Docker daemon reads its own architecture, consults the manifest list, and pulls the matching image transparently. `docker buildx imagetools inspect app:1.4.0` shows you the underlying manifest list and confirms both architectures are actually present before you rely on it in CI.

--push is required for multi-platform builds

A multi-platform buildx build can't be loaded into the local Docker image store the way a single-platform build can — `--load` only supports one platform at a time. You need `--push` to a registry, or split the build into single-platform stages if you need a local image to test immediately.

The build-time cost is real

Emulated builds are the main practical pain point. A Node or Python image with native dependencies (anything compiling C extensions) can see build times triple or worse under QEMU for the ARM64 leg. Two mitigations help: cache aggressively with `--cache-from`/`--cache-to` pointed at registry cache so repeated builds skip unchanged layers, and split CI so the amd64 build runs on a native amd64 runner while the arm64 build runs on a native arm64 runner (GitHub Actions now offers native ARM64 runners on most plans), merging the two into one manifest list at the end with `docker buildx imagetools create`.

Test on the actual target architecture

An emulated build can succeed while producing a binary with subtle architecture-specific bugs — alignment issues, differing floating-point behavior, or a dependency that silently falls back to a slower code path. Before shipping, pull and run the arm64 image on real ARM64 hardware at least once, don't trust the emulated build result alone.

Writing Dockerfiles that work cross-arch

Most Dockerfiles need no changes — `FROM node:20`, `FROM python:3.12-slim`, and similar official base images already ship multi-arch manifest lists, so the same `FROM` line resolves to the right architecture automatically. The place cross-arch bugs hide is explicit binary downloads: a Dockerfile that curls a specific released binary needs to select the URL based on `TARGETARCH`, a build argument buildx sets automatically to `amd64` or `arm64` inside the build.

ApproachBuild speedInfra needed
QEMU emulation, single runnerSlow for non-native archNone extra
Native runners per architectureFastARM64 + x86-64 runners
Registry build cacheFast on repeat buildsCache-capable registry

Wrapping up

Multi-arch builds are close to free to set up and expensive to run under emulation — the fix is native runners per architecture once build time actually starts hurting, not a reason to avoid multi-arch in the first place. Verify the manifest list, not just a green CI checkmark, before you trust that both architectures actually work.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.