Multi-architecture Docker images let a single tag serve different CPU types, most commonly linux/amd64 (Intel/AMD) and linux/arm64 (Apple Silicon, AWS Graviton, Ampere). With Docker Buildx you can build both architectures from one command, push a manifest list, and let registries and clients negotiate the correct image at pull time.
This guide walks through setting up Buildx, choosing platform targets, understanding local testing limits, avoiding QEMU pitfalls, publishing registry manifests, and automating releases in CI.
Workflow Overview
The end‑to‑end flow you will implement:
- Prepare Buildx
- Enable Buildx and create a dedicated builder instance.
- Optionally register QEMU emulators for cross‑building when native hardware is unavailable.
- Build and test locally
- Build per‑architecture images you can load locally for smoke tests.
- Validate basic application behavior and architecture labels.
- Build and push multi‑arch
- Build for linux/amd64 and linux/arm64 in a single command.
- Push per‑arch images plus a manifest list that references both.
- Automate in CI
- Provision Buildx in the CI job.
- Log in to the registry.
- Build, tag, and push multi‑arch images on versioned releases.
- Operate and troubleshoot
- Pin base images and tags for reproducibility.
- Inspect manifests and debug platform mismatches.
Buildx and platform targets
Docker Buildx extends docker build with multi‑platform support and advanced features.
- Platforms: Set explicit targets with
--platform linux/amd64,linux/arm64. You can also build one platform at a time. - Builder drivers: The
docker-containerdriver (default for Buildx) runs builds in an isolated container and handles multi‑platform builds well. - Manifests: When you push a multi‑platform build, the registry stores a manifest list that points to each per‑arch image. Clients automatically pull the best match for their platform.
Common commands:
# Check Buildx availability
docker buildx version
# Create and use a dedicated builder
docker buildx create --name multiarch --use
# Initialize (starts a build container if needed)
docker buildx inspect --bootstrap
# List builders
docker buildx ls
Local testing limits
Local testing has sharp edges with multi‑arch:
docker buildx build --loadcan load only a single‑arch image into your local Docker image store. It cannot load a manifest list.docker runuses your host architecture by default. You can request a different platform with--platform, but it will rely on emulation unless you have native hardware.- Emulated runs are slower and can mask or introduce issues. Use them for smoke tests only.
Examples:
# Build and load only arm64 locally (quick smoke test)
docker buildx build \
--platform linux/arm64 \
-t myorg/app:arm64-test \
--load .
# Verify architecture label
docker inspect --format '{{.Os}}/{{.Architecture}}' myorg/app:arm64-test
# Run with explicit platform (may use emulation)
docker run --rm --platform linux/arm64 myorg/app:arm64-test uname -m
QEMU caveats
QEMU emulation lets you build and run foreign‑arch images on your host, but keep in mind:
- It is slower; expect minutes to hours longer for large builds.
- Some workloads break under emulation (JIT compilers, low‑level syscalls, tight timing loops).
- Toolchains that detect CPU features at runtime may behave differently.
Register emulators when needed:
# Register emulators (requires privileged Docker)
docker run --privileged --rm tonistiigi/binfmt --install all
Recommendations:
- Prefer native builders for production builds. Use emulation only for early experimentation.
- Keep test suites short when running under QEMU (smoke tests, not full regression packs).
- If an emulated build fails in odd ways, retry on native hardware to confirm.
Registry manifests and tagging
When you push a multi‑platform build, Buildx creates per‑arch images and a manifest list under your tag.
Inspect a manifest:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/myorg/app:1.0.0 \
-t registry.example.com/myorg/app:latest \
--push .
# Inspect the manifest list
docker manifest inspect registry.example.com/myorg/app:1.0.0 | jq '.'
Tagging tips:
- Use immutable version tags (e.g.,
1.0.0) and a movinglatestfor convenience. - For debugging, publish arch‑specific tags in addition to the multi‑arch tag:
# Push arch‑specific images separately (debug‑friendly)
docker buildx build --platform linux/amd64 -t myorg/app:1.0.0-amd64 --push .
docker buildx build --platform linux/arm64 -t myorg/app:1.0.0-arm64 --push .
Dockerfile examples
Start simple. Avoid architecture surprises by using minimal, widely supported base images.
Minimal multi‑stage example (Go)
# syntax=docker/dockerfile:1
# Build stage
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Avoid CGO to keep libc differences out of your images
ENV CGO_ENABLED=0
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w" -o /out/app ./cmd/app
# Runtime stage
FROM scratch
COPY --from=build /out/app /app
ENTRYPOINT [
]
Notes:
- Use
--platformin your build command; Buildx injectsTARGETOSandTARGETARCHautomatically. - Pin base images by digest for reproducibility.
Node.js example
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM --platform=$BUILDPLATFORM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist /app/dist
ENV NODE_ENV=production
CMD ["node", "dist/index.js" ]
Docker Compose usage
Compose can run services for a specific platform, useful on mixed fleets or during testing.
version: "3.9"
services:
web:
image: myorg/app:1.0.0
platform: linux/arm64 # force arm64 on capable hosts
ports:
- "8080:8080"
Tip: Only set platform in Compose when you have a reason (such as validating arm64 behavior). Otherwise let the engine pick the native match from the manifest.
CI release workflow
Automate multi‑arch builds in CI so releases are consistent.
Example using the Docker CLI in a generic CI job:
# Ensure Docker is available and DinD is running if needed
# Optional: register emulators for cross‑builds
docker run --privileged --rm tonistiigi/binfmt --install all
# Create and use a Buildx builder
docker buildx create --name ci-builder --use
docker buildx inspect --bootstrap
# Log in to your registry
printf "%s" "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin registry.example.com
# Build and push multi‑arch images
APP_IMAGE="registry.example.com/myorg/app"
VERSION="$CI_COMMIT_TAG" # e.g., 1.0.0
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t "$APP_IMAGE:$VERSION" \
-t "$APP_IMAGE:latest" \
--push .
# Optionally publish arch‑specific debug tags
for P in linux/amd64 linux/arm64; do
SFX=$(echo $P | cut -d'/' -f2)
docker buildx build --platform $P -t "$APP_IMAGE:$VERSION-$SFX" --push .
done
Tips:
- Run release builds on native runners where possible to avoid QEMU performance hits.
- Cache layers per architecture. Consider
--cache-toand--cache-fromwith a registry cache.
Local pilot plan
Ship a small, inspectable pilot before expanding to all services:
- Pick one simple containerized service.
- Make its Dockerfile multi‑arch friendly (no arch‑specific binaries, minimal base image).
- Build locally for amd64 and arm64. Load one architecture at a time with
--loadand run a smoke test. - Push to a test registry with versioned and
latesttags. - Verify the manifest and platform resolution:
docker manifest inspect registry.example.com/myorg/app:test | jq '.manifests[].platform'
- In CI, automate the exact same steps for tags that match a release pattern.
This narrow, measurable pilot is easy to inspect locally and reduces rollout risk.
Troubleshooting
- exec format error when running: You pulled an image for the wrong architecture. Re‑pull with a multi‑arch tag or run with
--platformthat matches the image. - no match for platform in manifest: The tag is single‑arch. Rebuild and push with
--platform linux/amd64,linux/arm64. - qemu: unsupported syscall or random crashes under emulation: Retry on native hardware, reduce test scope under QEMU, or adjust your runtime (disable JITs where possible).
- Slow builds: Use native builders, trim layers, and cache dependencies aggressively.
- Base image missing for one arch: Choose a base image that supports both architectures or build from source in your first stage.
Conclusion
Multi‑architecture images let you serve modern arm64 machines and existing amd64 hosts with one tag. Buildx provides a clean way to produce per‑arch images and a manifest list, but local testing has limits and QEMU is best kept to smoke tests. Start with a small pilot, automate the build and push in CI, and use clear tagging so your production pulls always resolve to the correct image.