← Back to DevBytes

Docker Multi-Architecture Builds: Best Practices and Common Pitfalls

Introduction: What Are Multi-Architecture Docker Builds?

Multi-architecture Docker builds produce a single container image tag that can run on multiple CPU architectures, such as linux/amd64, linux/arm64, linux/arm/v7, and others. Instead of maintaining separate image tags like myapp:1.0-amd64 and myapp:1.0-arm64, you push an image manifest list — often called a "fat manifest" — that automatically resolves to the correct binary and filesystem layout for the host architecture. Docker, Kubernetes, and other container runtimes transparently pull the right layer set.

This capability is powered by Docker BuildKit and the docker buildx CLI plugin, which replaced the legacy docker build for most multi-platform workflows. Understanding how to configure, build, and distribute these images is now essential as ARM-based cloud instances (AWS Graviton, Ampere Altra) and Apple Silicon Macs become mainstream development environments.

Why Multi-Architecture Builds Matter

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Adopting multi-architecture builds provides concrete benefits:

Without multi-arch images, teams often maintain separate registries or tags, leading to confusion and manual overhead. A unified manifest list solves this cleanly.

How Multi-Architecture Builds Work

Docker achieves multi-architecture support through image manifests. A manifest list contains pointers to individual, architecture-specific image manifests. When a client pulls myimage:latest, the registry inspects the manifest list, selects the entry matching the client's OS and architecture, and serves only that specific image. This process is invisible to the user.

To build these images, you need a builder capable of producing binaries and filesystems for each target platform. There are two primary strategies:

Modern Docker BuildKit supports both. The typical workflow uses docker buildx to create a builder instance, then invoke a single command that builds all platforms in parallel and assembles the final manifest list.

Setting Up a Multi-Architecture Builder

First, ensure docker buildx is available (Docker Desktop includes it by default; on Linux you may install it via the docker-buildx-plugin package). Create a dedicated builder with multi-platform support:

# Create a new builder that can target multiple architectures
docker buildx create --name multiplatform --driver docker-container --use

# Inspect the builder to verify supported platforms
docker buildx inspect --bootstrap

The --bootstrap flag starts the builder container and confirms the platforms it can handle. By default, if your host kernel supports binfmt_misc and you've registered QEMU binaries, the builder can emulate all common architectures. To add QEMU support on Linux:

# Install QEMU user static binaries and register them via binfmt_misc
docker run --privileged --rm tonistiigi/binfmt --install all

Now you can build images for linux/amd64, linux/arm64, linux/arm/v7, linux/ppc64le, linux/s390x, and more.

Building a Multi-Architecture Image: Basic Example

Assume a simple Go application with the following Dockerfile:

# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY main.go .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /hello main.go

FROM alpine:3.18
COPY --from=builder /hello /usr/local/bin/hello
CMD ["hello"]

The ARG TARGETOS TARGETARCH are automatically supplied by BuildKit based on the target platform. The Go compiler then cross-compiles natively, no QEMU needed during the build stage. The final stage uses a base image (alpine:3.18) that has a manifest list itself, so it resolves correctly for each platform.

Build and push with a single command:

docker buildx build \
  --platform linux/amd64,linux/arm64,linux/arm/v7 \
  --tag yourregistry/hello:1.0 \
  --push \
  .

This builds all three platforms in parallel (if resources allow), creates a manifest list, and pushes it to the registry. If you only want local images for testing, omit --push and add --load, but note that --load only works for a single platform; for multi-platform local testing you must push to a registry (or use a local registry container).

Best Practices for Multi-Architecture Builds

1. Use a Dedicated Builder Instance

Keep a dedicated docker-container driver builder for multi-platform work. The default Docker builder (docker driver) only supports the native host architecture. Create one as shown above and switch to it when needed.

2. Prefer Cross-Compilation Over Emulation

For compiled languages (Go, Rust, C, C++, Zig), leverage cross-compilation during the build stage. Emulation with QEMU is convenient but often 10x slower and can introduce subtle bugs due to syscall translation. Use ARG TARGETARCH and set compiler targets accordingly.

3. Use Multi-Stage Builds with Architecture-Aware Base Images

Ensure every FROM image in your Dockerfile is itself a multi-arch manifest list. Most official Docker Hub images (alpine, ubuntu, golang, node) are already multi-arch. If you use a custom base image, publish it as multi-arch first. The final runtime stage should also be multi-arch.

4. Tag with Care and Avoid "Latest" Confusion

Use explicit version tags (1.0.0, 1.0) alongside a floating tag like latest if desired. When pushing a multi-arch manifest list, all tags point to the manifest list. Avoid architecture-specific tags unless for debugging.

5. Validate with docker manifest inspect

After pushing, verify the manifest list is correctly assembled:

docker manifest inspect yourregistry/hello:1.0

You'll see a JSON output listing the platform entries. This confirms all architectures are present.

6. Integrate into CI/CD with Caching

In CI pipelines, use --cache-from and --cache-to to speed up repeated builds. A typical GitHub Actions workflow might:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --cache-from type=registry,ref=yourregistry/hello:buildcache \
  --cache-to type=registry,ref=yourregistry/hello:buildcache,mode=max \
  --tag yourregistry/hello:${VERSION} \
  --push \
  .

7. Test on Real Hardware (or Accurate Emulation)

Even if QEMU emulation succeeds, test on actual ARM hardware when possible. Some issues only surface on native hardware (e.g., alignment faults, timing-sensitive bugs). Use cloud ARM instances (Graviton, Ampere) or physical devices like Raspberry Pi.

Common Pitfalls and How to Avoid Them

Pitfall 1: Emulation Slowness and Timeouts

Symptom: Builds take 30+ minutes, CI jobs timeout.

Solution: Switch to cross-compilation. For Go, Rust, and many other ecosystems, a few ARG and ENV lines replace heavy emulation. If you must use QEMU, increase CI timeouts and consider splitting builds across native runners (e.g., use a CI matrix with amd64 and arm64 hosts).

Pitfall 2: Hard-Coded Architecture Strings

Scripts or Dockerfile instructions that check uname -m and download specific binaries break under multi-arch builds. Use ARG TARGETARCH and map it to the correct download URL. For example:

ARG TARGETARCH
RUN case ${TARGETARCH} in \
      amd64)  ARCH=x86_64;; \
      arm64)  ARCH=aarch64;; \
      arm)    ARCH=armhf;; \
      *) echo "unsupported architecture"; exit 1;; \
    esac && \
    curl -L "https://example.com/bin/${ARCH}/mybinary" -o /usr/local/bin/mybinary

Pitfall 3: Missing Multi-Arch Base Images

If a third-party image you depend on (e.g., myinternalservice:2) is only built for amd64, your multi-arch build will fail or produce a broken arm64 image. Always check with docker manifest inspect whether the base image is multi-arch. If not, either request upstream support or build and push your own multi-arch base image.

Pitfall 4: Forgetting to Push the Manifest List

If you use --load or forget --push, only a local image for the native architecture exists. The manifest list never reaches the registry. Always use --push for multi-platform distributions; for local testing, push to a local registry or use docker buildx imagetools create manually.

Pitfall 5: Confusing Single-Platform --load with Multi-Platform

The --load flag only supports loading a single image into the local Docker engine. Attempting --load with multiple platforms will error. For local multi-platform tests, spin up a temporary registry:

docker run -d -p 5000:5000 --name registry registry:2
docker buildx build --platform linux/amd64,linux/arm64 --tag localhost:5000/hello:1.0 --push .
docker pull localhost:5000/hello:1.0

Pitfall 6: RUN Instructions and Emulation Breakage

During emulated builds, some RUN commands (like those using network services, complex shell scripts, or apt-get in certain configurations) may hang or fail. This is often due to QEMU translation of syscalls. Workarounds: use cross-compilation for the build stage, keep RUN steps minimal in the final stage, or pre-build dependencies into a base image using native hardware.

Pitfall 7: Not Cleaning Up Builder Instances

BuildKit builders consume resources. Periodically prune with:

docker buildx prune --all --verbose

And remove unused builders with docker buildx rm. This frees disk space and keeps the environment tidy.

Advanced: Using Docker Buildx with CI/CD Matrix Builds

For ultra-fast builds without emulation, many teams use a CI matrix strategy: run a native amd64 runner and a native arm64 runner in parallel, each building for its own architecture. After both succeed, a final step merges the images into a manifest list using docker buildx imagetools create.

Example steps:

# On amd64 runner
docker build --platform linux/amd64 --tag myregistry/myapp:${VERSION}-amd64 --push .

# On arm64 runner
docker build --platform linux/arm64 --tag myregistry/myapp:${VERSION}-arm64 --push .

# Merge step (any runner with buildx)
docker buildx imagetools create \
  --tag myregistry/myapp:${VERSION} \
  myregistry/myapp:${VERSION}-amd64 \
  myregistry/myapp:${VERSION}-arm64

This approach gives the best performance and avoids QEMU entirely but requires maintaining separate per-architecture tags before merging. The final tag is a clean manifest list.

Conclusion

Multi-architecture Docker builds are no longer optional in a heterogeneous computing landscape. By leveraging Docker BuildKit and buildx, you can deliver a single image tag that seamlessly adapts to amd64, arm64, and beyond. The key takeaways: set up a dedicated builder, prefer cross-compilation, validate your base images, use proper CI/CD caching, and always test on native hardware when possible. Avoid the common pitfalls of emulation slowdown, hard-coded architectures, and forgotten manifest pushes. With these best practices, you'll provide a frictionless experience for developers on Apple Silicon, reduce cloud costs on ARM instances, and future-proof your container deployments.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles