How do you build multi-platform Docker images (amd64, arm64) for production deployment? Explain BuildKit's cross-compilation strategy, QEMU emulation, and native cross-compilation. What are the performance implications?
Quick Answer
Use 'docker buildx build --platform linux/amd64,linux/arm64' which creates a manifest list pointing to platform-specific images. BuildKit supports QEMU user-mode emulation (slow but universal) or native cross-compilation (fast but language-specific). For Go/Rust, cross-compile natively. For interpreted languages, QEMU is sufficient.
Detailed Answer
Multi-Platform Image Architecture
A multi-platform image is actually a manifest list (OCI image index) that contains references to platform-specific image manifests. When a client pulls the image, Docker selects the correct platform variant automatically. The registry stores separate layers for each architecture.
Build Strategies
1. QEMU Emulation (Universal, Slow): BuildKit uses QEMU user-mode emulation to run foreign-architecture binaries on the build host. When building an arm64 image on an amd64 host, every RUN instruction executes through QEMU's arm64 emulator. This is transparent but 5-20x slower than native execution. - Setup: docker run --privileged --rm tonistiigi/binfmt --install all - Use case: Building images where most work is in COPY/ADD (interpreted languages), not RUN (compilation)
2. Native Cross-Compilation (Fast, Language-Specific): For compiled languages (Go, Rust, C/C++), set cross-compilation flags instead of emulating the target architecture. The compiler runs natively on the host but produces target-architecture binaries. - Go: Set GOARCH=arm64 while running on amd64 - Rust: Use --target aarch64-unknown-linux-musl - C/C++: Use cross-compiler toolchain (aarch64-linux-gnu-gcc)
3. Multi-Node Build (Fastest, Complex): Use BuildKit with remote builder nodes - an amd64 builder and an arm64 builder. Each platform builds natively. BuildKit orchestrates both and merges results into a manifest list.
Performance Comparison (building a Go service)
- Native amd64 build: 45 seconds - QEMU arm64 build on amd64: 8 minutes (10x slower) - Cross-compilation arm64 on amd64: 50 seconds (negligible overhead) - Native arm64 builder node: 45 seconds
Code Example
# Setup buildx for multi-platform builds
docker buildx create --name multiarch --driver docker-container --bootstrap
docker buildx use multiarch
# Install QEMU for emulation
docker run --privileged --rm tonistiigi/binfmt --install all
# Build multi-platform image
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myregistry.io/myapp:v1.2.3 \
--push .
# Optimized Dockerfile using native cross-compilation for Go
# syntax=docker/dockerfile:1.7
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Cross-compile natively (no QEMU needed for this step)
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -ldflags='-s -w' -o /server ./cmd/server
# Runtime image - automatically selects correct platform base
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
ENTRYPOINT ["/server"]
# Using multiple native builder nodes (fastest)
docker buildx create --name multi-native \
--node amd64-builder --platform linux/amd64 \
--driver-opt "image=moby/buildkit:latest"
docker buildx create --append --name multi-native \
--node arm64-builder --platform linux/arm64 \
ssh://user@arm64-host \
--driver-opt "image=moby/buildkit:latest"
# Inspect manifest list
docker buildx imagetools inspect myregistry.io/myapp:v1.2.3
# Shows: linux/amd64, linux/arm64 with separate digests
# CI/CD: GitHub Actions multi-platform build
- name: Build and push multi-platform
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: myregistry.io/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxInterview Tip
Multi-platform builds are increasingly important with ARM-based servers (AWS Graviton, Apple Silicon). The key insight is the performance difference between QEMU emulation and native cross-compilation. For Go services, always use the TARGETARCH/TARGETOS build args pattern - it's orders of magnitude faster than QEMU. Know that --platform=$BUILDPLATFORM on the FROM line means 'run this stage on the build host architecture'.