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 matchTARGETARCH. Use the build args instead of hard-codingamd64. - 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.