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 routeslinux/arm64workloads to native ARM hardware andlinux/amd64tasks 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: SpecifyingFROM --platform=$BUILDPLATFORMensures that the compiler toolchain runs natively on the host runner's architecture. The compiler itself outputs binaries targetingGOARCH=${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.