Skip to content
C Codeloom
Docker

Docker Buildx and Multi-Arch Images: amd64 plus arm64

Build container images that run on both x86 and ARM with Docker Buildx, QEMU, and registry manifests, without doubling your CI complexity.

·4 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • Why multi-arch matters now
  • How buildx and BuildKit fit together
  • Native vs emulated builds
  • Manifest lists in registries
  • CI patterns for multi-arch

Prerequisites

  • Familiar with terminals and YAML

What and Why

ARM is no longer niche. Apple Silicon, AWS Graviton, and Ampere mean a chunk of your developers and your production fleet now want arm64 images, while x86 servers still demand amd64. Shipping one image that quietly resolves to the right architecture is now table stakes.

Docker Buildx, backed by BuildKit, makes multi-arch builds practical. A single docker buildx build --platform linux/amd64,linux/arm64 produces both and pushes a single tag that does the right thing for each puller.

Mental Model

Tag: app:1.2.0
|
v
Manifest List (a pointer to per-arch manifests)
|--- linux/amd64  -> image manifest -> layers
|--- linux/arm64  -> image manifest -> layers
+--- linux/arm/v7 -> image manifest -> layers

Pull on M-series Mac  -> registry returns arm64 manifest
Pull on EC2 c7i       -> registry returns amd64 manifest
Multi-arch image as a manifest list

Buildx orchestrates a BuildKit instance per target platform. If you have native builders for each, builds are fast. If you do not, QEMU emulates the missing platform - slower but functional.

Hands-on Example

Enable buildx and create a builder:

docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap

A Dockerfile that uses BuildKit’s automatic platform args:

# syntax=docker/dockerfile:1.7
FROM --platform=$BUILDPLATFORM golang:1.22 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
    go build -o /out/app ./cmd/app

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

The trick is --platform=$BUILDPLATFORM on the build stage. The compiler runs natively on the host arch but cross-compiles to the target. This is far faster than emulating the build itself.

Build and push:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/acme/app:1.2.0 \
  --push .

Inspect the result:

docker buildx imagetools inspect ghcr.io/acme/app:1.2.0

You will see a manifest list with both platforms. Pulling from an M2 laptop will fetch the arm64 image; pulling from an x86 server fetches amd64.

In GitHub Actions:

- 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@v6
  with:
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ghcr.io/acme/app:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

Common Pitfalls

  • Emulating everything via QEMU. Builds get 5 to 10x slower. Prefer cross-compilation in code-heavy stages, then a tiny native final stage.
  • Native bindings that miss an arch. A node native module or Python wheel may not have arm64 binaries. Verify dependency availability before you commit.
  • Single-arch base images. Some legacy images only publish amd64. Pulling on arm64 falls back to emulation at runtime, which is awful for perf.
  • Forgetting --push. Without it, multi-platform builds cannot save into the local Docker image store and the build appears to vanish.
  • Cache split across platforms. GHA cache without scoping can thrash. Use per-platform cache keys or rely on registry-backed cache (type=registry).

Production Tips

  • For best speed, run native builders per arch. A self-hosted ARM runner plus a self-hosted x86 runner beats QEMU emulation every time.
  • Use --cache-to=type=registry,mode=max,ref=ghcr.io/acme/app:cache to share layers across CI runs and developers.
  • Make arm64 a CI matrix dimension for tests too. It is the only way to catch architecture-specific bugs early.
  • Adopt graviton-class instances in production wherever your stack supports it. Often 20 percent cheaper and 20 percent faster.
  • Lock the image to specific platforms in deployment manifests if you have non-portable native code: Kubernetes nodeSelector: kubernetes.io/arch: arm64.

Wrap-up

Multi-arch images used to be exotic. Today they are the default expectation - developers on ARM laptops, production on ARM servers, legacy systems on x86 - and Buildx makes them straightforward. Cross-compile where you can, run native builders where you must, push a single manifest list, and let the registry sort out the right bytes per puller.