← Back to Blog

Optimizing Multi-Architecture Docker Builds with Buildx



Optimizing Multi-Architecture Docker Builds with Buildx

If you construct container images targeting both ARM64 (such as Apple Silicon or AWS Graviton) and x86_64 server architectures, you have likely experienced how slow docker buildx performs under QEMU emulation. Compiling ARM binaries on an x86 CI runner via emulation frequently turns a 2-minute build into a 20-minute bottleneck.

Here is how to accelerate multi-architecture builds by leveraging native build nodes, cross-compilation techniques, and remote build caching.

1. Using a Remote Native ARM64 Builder

While application emulation (QEMU) works out of the box, it incurs significant CPU overhead. The most performant solution is to dispatch ARM build stages directly to an ARM server over SSH while processing x86 tasks on your local CI runner.

# Create a new builder instance
docker buildx create --name multi-arch-builder --use

# Add the local x86_64 node docker buildx create --name multi-arch-builder \ --append \ --node x86_node \ --platform linux/amd64

# Add a remote ARM64 node over SSH docker buildx create --name multi-arch-builder \ --append \ --node arm64_node \ --platform linux/arm64 \ ssh://ci-user@arm-builder.local

Note: When attaching a remote SSH node, BuildKit automatically routes linux/arm64 workloads to native ARM hardware and linux/amd64 tasks to the local host machine. Ensure that your CI runner's SSH key is configured for passwordless authentication to the remote ARM server.

2. Cross-Compiling Inside the Dockerfile

When dedicated ARM build hardware is unavailable, cross-compilation serves as an effective alternative. Compiled languages such as Go, Rust, and C/C++ support native cross-compilation without requiring an emulated CPU architecture.

You can leverage Buildx automatic platform arguments ($BUILDPLATFORM, $TARGETOS, $TARGETARCH) directly within your Dockerfile:

# Run the compiler on the host runner's native CPU architecture
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder

# Automatically injected by Buildx ARG TARGETPLATFORM ARG TARGETOS ARG TARGETARCH

WORKDIR /app COPY go.mod go.sum ./ RUN go mod download

COPY . . # Cross-compile to the target architecture natively RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -o /api-server main.go

# Final runtime image matches target platform FROM --platform=$TARGETPLATFORM alpine:latest COPY --from=builder /api-server /api-server ENTRYPOINT ["/api-server"]

Why this works: Specifying FROM --platform=$BUILDPLATFORM ensures that the compiler toolchain runs natively on the host runner's architecture. The compiler itself outputs binaries targeting GOARCH=${TARGETARCH}. Because the compilation process bypasses QEMU execution entirely, build speeds match those of native compilation.

3. Remote Registry Build Caching

On ephemeral CI runners (such as GitHub Actions or GitLab CI), local Docker build caches are cleared between execution jobs. BuildKit allows you to export build cache manifests directly to your container registry so subsequent pipeline runs reuse cached layers across multiple architectures.

docker buildx build \
 --platform linux/amd64,linux/arm64 \
 --tag myrepo/api-server:latest \
 --cache-from type=registry,ref=myrepo/api-server:buildcache \
 --cache-to type=registry,ref=myrepo/api-server:buildcache,mode=max \
 --push \
 .
Tip: The mode=max option instructs BuildKit to cache all intermediate compilation stages (including multi-stage build layers), rather than caching only final output image layers. Storing build caches within a remote registry decouples cache persistence from temporary runner storage.