Multi-Architecture Docker Builds: Supporting ARM and x86 from One Dockerfile
The days of x86 monoculture are over. Apple Silicon Macs run ARM. AWS Graviton instances offer better price-performance on ARM. Raspberry Pi clusters are legitimate edge deployment targets. And your Docker images need to work on all of them. Multi-architecture builds let you produce images for multiple CPU architectures from a single Dockerfile, stored under a single tag in your registry. When someone runs docker pull, Docker automatically fetches the correct architecture.
This guide covers everything you need to build, test, and deploy multi-architecture Docker images in production.
Understanding Multi-Platform Images
A multi-platform Docker image is actually a manifest list (also called an image index) that points to multiple platform-specific images:
# Inspect a multi-platform image
docker buildx imagetools inspect nginx:alpine
# Name: docker.io/library/nginx:alpine
# MediaType: application/vnd.oci.image.index.v1+json
# Digest: sha256:abc123...
#
# Manifests:
# Name: docker.io/library/nginx:alpine@sha256:def456...
# MediaType: application/vnd.oci.image.manifest.v1+json
# Platform: linux/amd64
#
# Name: docker.io/library/nginx:alpine@sha256:ghi789...
# MediaType: application/vnd.oci.image.manifest.v1+json
# Platform: linux/arm64
#
# Name: docker.io/library/nginx:alpine@sha256:jkl012...
# MediaType: application/vnd.oci.image.manifest.v1+json
# Platform: linux/arm/v7
When you docker pull nginx:alpine on an ARM machine, Docker fetches the linux/arm64 variant. On an x86 machine, it fetches linux/amd64. Same tag, different binaries.
| Platform | Description | Common Hardware |
|---|---|---|
linux/amd64 |
64-bit x86 (Intel/AMD) | Most cloud servers, Intel Macs, PCs |
linux/arm64 |
64-bit ARM (AArch64) | Apple Silicon Macs, AWS Graviton, Ampere |
linux/arm/v7 |
32-bit ARM v7 | Raspberry Pi 2/3/4 (32-bit OS) |
linux/arm/v6 |
32-bit ARM v6 | Raspberry Pi Zero/1 |
linux/s390x |
IBM Z mainframe | IBM z15, LinuxONE |
linux/ppc64le |
PowerPC 64-bit LE | IBM Power Systems |
Setting Up Buildx
Docker Buildx is the CLI plugin for BuildKit that enables multi-platform builds. It comes pre-installed with Docker Desktop and recent Docker Engine versions.
# Check buildx version
docker buildx version
# github.com/docker/buildx v0.13.0
# List existing builders
docker buildx ls
# NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
# default docker
# default default running v0.12.5 linux/amd64
# desktop-linux docker
# desktop-linux desktop-linux running v0.12.5 linux/amd64, linux/arm64
# Create a new builder with multi-platform support
docker buildx create --name multiarch --driver docker-container --bootstrap --use
# Verify the builder supports multiple platforms
docker buildx inspect multiarch
# Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3,
# linux/arm64, linux/arm/v7, linux/arm/v6,
# linux/386, linux/ppc64le, linux/s390x
QEMU Emulation
When building for a different architecture than your host, BuildKit uses QEMU to emulate the target CPU. This must be set up once:
# Install QEMU emulators (one-time setup)
docker run --privileged --rm tonistiigi/binfmt --install all
# Verify QEMU is registered for all architectures
docker run --privileged --rm tonistiigi/binfmt
# Supported: aarch64, arm, riscv64, ppc64le, s390x
# Alternatively, install specific architectures only
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
Building Multi-Platform Images
Basic Multi-Platform Build
# Build for amd64 and arm64, push to registry
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/myapp:latest \
--push \
.
# Build for multiple platforms including 32-bit ARM
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
-t registry.example.com/myapp:latest \
--push \
.
--load (which loads into the local Docker daemon). You must either --push to a registry or use --output type=local. This is because the local daemon can only hold one platform at a time.
Platform-Aware Dockerfiles
BuildKit provides automatic build arguments for the target platform. Use them to write architecture-aware Dockerfiles:
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
# BUILDPLATFORM = the platform of the build machine
# TARGETPLATFORM = the platform we're building FOR
# TARGETOS = target OS (linux)
# TARGETARCH = target architecture (amd64, arm64, arm)
# TARGETVARIANT = target variant (v7 for arm/v7)
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -ldflags="-s -w" -o /app ./cmd/server
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app /usr/local/bin/app
ENTRYPOINT ["app"]
The critical detail here is FROM --platform=$BUILDPLATFORM on the builder stage. This runs the Go compiler natively on your build machine (fast), while cross-compiling for the target architecture. Without this, the entire build runs under QEMU emulation (slow).
Handling Architecture-Specific Dependencies
Sometimes you need different packages or configurations per architecture:
# syntax=docker/dockerfile:1
FROM alpine:3.19
ARG TARGETARCH
# Install architecture-specific packages
RUN case "$TARGETARCH" in \
amd64) apk add --no-cache package-x86 ;; \
arm64) apk add --no-cache package-arm ;; \
arm) apk add --no-cache package-armv7 ;; \
esac
# Download architecture-specific binaries
RUN wget -O /usr/local/bin/tool \
"https://releases.example.com/tool-linux-${TARGETARCH}" && \
chmod +x /usr/local/bin/tool
CI/CD Integration
GitHub Actions
# .github/workflows/multi-arch.yml
name: Multi-Arch Build
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
GitLab CI
# .gitlab-ci.yml
build-multiarch:
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_BUILDKIT: "1"
before_script:
- docker run --privileged --rm tonistiigi/binfmt --install all
- docker buildx create --use --name multiarch
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker buildx build
--platform linux/amd64,linux/arm64
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
-t $CI_REGISTRY_IMAGE:latest
--push .
only:
- tags
Faster CI: Parallel Native Builds
QEMU emulation in CI is slow. For large projects, build each architecture on a native runner and combine the results:
# .github/workflows/multi-arch-fast.yml
name: Multi-Arch Build (Native)
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
include:
- runner: ubuntu-latest
platform: linux/amd64
- runner: ubuntu-latest-arm64
platform: linux/arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
platforms: ${{ matrix.platform }}
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,scope=${{ matrix.platform }},mode=max
manifest:
needs: build
runs-on: ubuntu-latest
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list
run: |
docker buildx imagetools create -t ghcr.io/${{ github.repository }}:${{ github.ref_name }} \
ghcr.io/${{ github.repository }}:${{ github.ref_name }}-amd64 \
ghcr.io/${{ github.repository }}:${{ github.ref_name }}-arm64
ARM-Specific Considerations
Base Image Compatibility
Not all base images support ARM. Always verify before using:
# Check if an image supports your target platform
docker buildx imagetools inspect python:3.12-slim
# Look for linux/arm64 in the manifests
# Alpine: Excellent ARM support (arm64, armv7, armv6)
# Debian/Ubuntu: Good ARM support (arm64, armv7)
# distroless: ARM64 support
# Many third-party images: x86 only!
Alpine musl Compatibility on ARM
Some applications have issues with Alpine's musl libc on ARM. If you encounter segfaults or mysterious crashes on ARM Alpine, try Debian-based images:
# If Alpine causes issues on ARM:
# FROM node:20-alpine # May have problems
FROM node:20-slim # Debian-based, more compatible
Raspberry Pi Deployment
Raspberry Pi is a popular target for Docker containers in home labs and edge computing:
# Raspberry Pi 4/5 (64-bit OS): linux/arm64
# Raspberry Pi 3/4 (32-bit OS): linux/arm/v7
# Raspberry Pi Zero/1: linux/arm/v6
# Check your Pi's architecture
uname -m
# aarch64 = arm64
# armv7l = arm/v7
# armv6l = arm/v6
# Pull and run (Docker automatically selects the right arch)
docker pull myapp:latest
docker run -d --name myapp -p 8080:8080 myapp:latest
# Memory considerations for Pi
docker run -d --memory 256m --memory-swap 512m myapp:latest
Optimizing Images for ARM
# Use lightweight base images
FROM alpine:3.19 # 7MB vs 77MB for Ubuntu
# Minimize installed packages
RUN apk add --no-cache ca-certificates
# For Go apps, use static binaries with scratch
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
Cloud ARM Instances
Major cloud providers offer ARM instances at lower cost:
| Provider | ARM Instance Type | Typical Savings |
|---|---|---|
| AWS | Graviton (t4g, m7g, c7g, r7g) | 20-40% vs x86 equivalent |
| Google Cloud | Tau T2A (Ampere Altra) | 20-30% |
| Azure | Dpsv5, Dplsv5 (Ampere Altra) | 20-30% |
| Oracle Cloud | A1 (Ampere Altra) | Free tier includes 4 ARM cores |
| Hetzner | CAX (Ampere Altra) | 30-40% |
By building multi-arch images, you can seamlessly deploy to ARM instances without any application changes. Just pull your image and run it—Docker handles the architecture selection automatically.
Testing Multi-Architecture Images
Local Testing with QEMU
# Test an ARM image on your x86 machine
docker run --platform linux/arm64 --rm myapp:latest --version
# Run a full integration test under ARM emulation
docker run --platform linux/arm64 --rm \
-v $(pwd)/tests:/tests \
myapp:latest /tests/run-tests.sh
Testing Matrix in CI
# Test each platform separately
jobs:
test:
strategy:
matrix:
platform: [linux/amd64, linux/arm64]
steps:
- uses: docker/setup-qemu-action@v3
- run: |
docker run --platform ${{ matrix.platform }} --rm \
myapp:latest ./run-tests.sh
Verifying the Manifest
# After pushing, verify the multi-platform manifest
docker buildx imagetools inspect registry.example.com/myapp:latest
# Verify a specific platform's image digest
docker manifest inspect registry.example.com/myapp:latest
# Pull a specific platform (for testing)
docker pull --platform linux/arm64 registry.example.com/myapp:latest
docker inspect myapp:latest | jq '.[0].Architecture'
# "arm64"
Common Pitfalls
- Forgetting
--platform=$BUILDPLATFORMon builder stage: Without this, the entire build runs under QEMU, making it 3-10x slower - Using architecture-specific binary downloads: Hard-coded URLs like
tool-linux-amd64break on ARM. UseTARGETARCHbuild args - CGO dependencies: C libraries must be cross-compiled or use
CGO_ENABLED=0for pure Go - Base image not supporting ARM: Always check multi-platform support before choosing a base image
- Testing only on one architecture: A build that succeeds on amd64 can fail on arm64 due to different alignment requirements or missing packages
When managing multi-architecture deployments across different hosts, platforms like usulnet help track which architectures are running where, ensuring the right images are deployed to the right hardware.
Conclusion
Multi-architecture Docker builds are no longer optional for serious projects. The combination of Apple Silicon development machines, cost-effective ARM cloud instances, and edge deployment on devices like Raspberry Pi means your images need to support at least linux/amd64 and linux/arm64. With BuildKit and buildx, this is straightforward: use cross-compilation where possible (Go, Rust), fall back to QEMU emulation for everything else, and always test both architectures in your CI pipeline.