Servers aren’t all x86 anymore: ARM machines (cloud ARM instances, Raspberry Pi, Apple Silicon laptops) run many workloads. An image built on an x86 VPS won’t run on ARM, and vice versa (“exec format error”). docker buildx builds multi-architecture images: one tag, with variants for each platform. Docker pulls the right one automatically.

How multi-arch images work

A multi-arch tag points to a manifest list (image index) containing one image per platform:

docker buildx imagetools inspect nginx:1.27
# lists linux/amd64, linux/arm64, linux/arm/v7, …

When a server pulls nginx:1.27, Docker picks the variant matching its CPU.

Set up buildx

buildx ships with current Docker Engine and Docker Desktop:

docker buildx version
docker buildx ls

To build for other architectures on an x86 VPS, install QEMU emulation:

docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name multi --use
docker buildx inspect --bootstrap        # lists supported platforms

Build and push

Multi-platform images must be pushed to a registry (the local image store can’t hold all variants on the classic storage driver):

docker login registry.example.com
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/myapp:1.8.3 \
  --push .

Build for a single foreign platform, and load it locally for testing:

docker buildx build --platform linux/arm64 -t myapp:arm-test --load .
docker run --rm --platform linux/arm64 myapp:arm-test uname -m     # aarch64

See Docker private registry and Dockerfile tutorial.

Emulation vs native builders

Approach Speed Setup
QEMU emulation Slow for compile-heavy builds (often 5–20× slower) One command
Native nodes (an ARM server + an x86 server in one builder) Full speed Add remote builders over SSH
Cross-compilation in the Dockerfile Fast Language support needed (Go, Rust)

Add a native ARM node to your builder:

docker buildx create --name multi --node amd64 --platform linux/amd64 --use
docker buildx create --name multi --append --node arm64 --platform linux/arm64 ssh://user@arm-server

Cross-compiling Go avoids emulation entirely:

# 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 /app ./cmd/app

FROM gcr.io/distroless/static
COPY --from=build /app /app
ENTRYPOINT ["/app"]

Caching

Use registry or CI caches, so repeated builds are fast:

docker buildx build --platform linux/amd64,linux/arm64 \
  --cache-from type=registry,ref=registry.example.com/myapp:buildcache \
  --cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
  -t registry.example.com/myapp:1.8.3 --push .

See Docker build cache.

In CI (GitHub Actions)

The official actions (docker/setup-qemu-action, docker/setup-buildx-action, docker/build-push-action) set this up in a few lines, with platforms: linux/amd64,linux/arm64. See GitHub Actions deploy to VPS.

Pitfalls

  • Base images must support every target platform (check with imagetools inspect).
  • Downloaded binaries in RUN curl … must match TARGETARCH. Use the build args instead of hard-coding amd64.
  • Native extensions (Python wheels, Node modules) may compile from source on ARM, which is slow under emulation.
  • Test each architecture, at least with a smoke test.

Quick reference

Task Command
Enable emulation docker run --privileged --rm tonistiigi/binfmt --install all
Create builder docker buildx create --name multi --use
Build + push multi-arch docker buildx build --platform linux/amd64,linux/arm64 -t … --push .
Inspect a tag docker buildx imagetools inspect <image>
Target arch in Dockerfile ARG TARGETARCH

Related: multi-stage builds and Docker image size reduction.

  • Docker
  • DevOps
  • CI/CD