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
Tip: QEMU emulation makes builds work but is significantly slower than native compilation. ARM builds on an x86 host typically take 3-10x longer than native. For large projects, consider using native ARM build nodes or cross-compilation strategies.

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 \
  .
Warning: Multi-platform builds cannot use --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

  1. Forgetting --platform=$BUILDPLATFORM on builder stage: Without this, the entire build runs under QEMU, making it 3-10x slower
  2. Using architecture-specific binary downloads: Hard-coded URLs like tool-linux-amd64 break on ARM. Use TARGETARCH build args
  3. CGO dependencies: C libraries must be cross-compiled or use CGO_ENABLED=0 for pure Go
  4. Base image not supporting ARM: Always check multi-platform support before choosing a base image
  5. 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.