Build Multi-Architecture Docker Images for ARM64 and AMD64

Developing applications today requires supporting a diverse range of hardware. You likely write code on an ARM64-based Apple Silicon Mac, while your production environment runs on AMD64 Intel/AMD nodes in Kubernetes, or perhaps you are migrating to cost-effective AWS Graviton (ARM64) instances. If you build an image on one architecture and attempt to run it on another, you encounter the dreaded standard_init_linux.go:211: exec user process caused "exec format error". This error happens because the binary inside the container is incompatible with the host CPU's instruction set.

Docker Buildx solves this by allowing you to build, tag, and push images for multiple architectures simultaneously. By creating a multi-architecture manifest, you provide a single image tag (e.g., myapp:latest) that works everywhere. The Docker engine or container runtime automatically detects the host architecture and pulls the matching layer. This guide demonstrates how to configure your environment, use QEMU emulation, and orchestrate builds for a unified deployment strategy.

TL;DR — Enable Docker Buildx, install QEMU binfmt support, and run docker buildx build --platform linux/amd64,linux/arm64 -t user/app:latest --push . to create a manifest list that supports both Intel/AMD and ARM architectures.

Understanding Multi-Architecture Manifests

💡 Analogy: Think of a multi-architecture image as a universal vending machine. When a customer (the host server) approaches, the machine detects their language (CPU architecture) and provides the specific snack (binary) they can understand. The customer only sees one machine, but inside, there are separate compartments for different needs.

A "Docker image" is often perceived as a single entity, but under the hood of a multi-arch setup, it is a Manifest List (or OCI Index). This manifest is a JSON file that points to specific image digests for different platforms. When you run docker pull, the client checks your local architecture, looks up that architecture in the manifest list, and fetches the correct image layers. This abstraction simplifies CI/CD pipelines because you no longer need to manage separate tags like app:latest-arm64 and app:latest-amd64.

Docker Buildx uses the Moby BuildKit engine to handle these complex builds. Unlike the standard docker build command, Buildx supports "builders" that can perform cross-platform compilation. To achieve this on a single machine (e.g., building ARM64 on an Intel laptop), Buildx relies on QEMU (Quick EMUlator). QEMU provides user-mode emulation, allowing the kernel to run binaries designed for other architectures by translating instructions on the fly. While this translation introduces overhead, it is the most flexible way to ensure your container images are ready for any cloud provider or hardware configuration.

When You Need Multi-Architecture Support

The primary driver for multi-architecture images is the rise of AWS Graviton and Apple Silicon. Cloud providers now offer ARM-based instances that typically provide 20% to 40% better price-performance than their x86 counterparts. If your organization wants to reduce cloud spend by moving workloads to Graviton, your containerized applications must support linux/arm64. However, your development team likely still uses a mix of Intel-based Windows machines and M-series Macs. Providing a multi-arch image ensures that the exact same tag works during local development, staging, and production without manual intervention.

Edge computing is another significant use case. Devices like Raspberry Pi or specialized industrial IoT gateways frequently run on ARM32 or ARM64. If you are deploying microservices to a hybrid cluster—where some nodes are high-performance Intel Xeon servers and others are energy-efficient ARM nodes—a multi-architecture manifest is mandatory. Without it, your orchestrator (like Kubernetes) will fail to schedule pods on mismatched nodes, leading to ImagePullBackOff errors or runtime crashes. Standardizing on linux/amd64 and linux/arm64 covers over 95% of modern computing environments.

How to Build Multi-Arch Images with Buildx

Step 1: Verify Buildx Installation

First, ensure you are running a modern version of Docker (20.10+ includes Buildx by default). You can check the available version and verify that Buildx is installed as a CLI plugin by running the following command:

docker buildx version

If you are on Linux and don't see Buildx, you may need to install the docker-buildx-plugin package via your distribution's package manager. For Windows and macOS users, Docker Desktop includes everything out of the box.

Step 2: Enable QEMU for Cross-Platform Support

To build for ARM on an AMD64 machine (or vice versa), you need QEMU binfmt support. This allows the Linux kernel to recognize and execute non-native binaries. Run the following helper container to register the necessary interpreters:

docker run --privileged --rm tonistiigi/binfmt --install all

This registration persists until the host reboots. You can verify the support by checking if files exist in /proc/sys/fs/binfmt_misc/qemu-*.

Step 3: Create and Switch to a New Builder

The default Docker builder does not support the --platform flag for concurrent multi-arch builds. You must create a new builder instance using the docker-container driver:

docker buildx create --name multiarch-builder --use
docker buildx inspect --bootstrap

The --bootstrap flag ensures the builder container is started and lists all supported platforms, such as linux/amd64, linux/arm64, linux/riscv64, and more.

Step 4: Execute the Multi-Arch Build

Now you can build for multiple platforms. Note that you must use the --push flag or the --load flag cannot be used with multiple platforms (as a local Docker daemon can only store a single architecture image at a time under one tag). To store a multi-arch manifest, it must be pushed to a registry like Docker Hub, ECR, or GHCR.

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t your-username/multi-arch-app:v1.0 \
  --push .

BuildKit will parallelize the build. It pulls the base image for both architectures, runs the Dockerfile instructions (using emulation where needed), and assembles the final manifest list.

Step 5: Verify the Result

Once the push is complete, you can inspect the manifest list in the registry to confirm both architectures are present:

docker buildx imagetools inspect your-username/multi-arch-app:v1.0

You should see an output listing both linux/amd64 and linux/arm64 with their respective digests. This confirms that any client pulling this tag will receive the correct version.

Common Pitfalls and Troubleshooting

⚠️ Common Mistake: Attempting to run docker buildx build --platform ... --load. The standard Docker daemon's local image storage (prior to the containerd image store integration) does not support multi-architecture manifest lists. You will get an error. Use --push to send it to a registry, or build one architecture at a time if you need it locally.

One frequent issue is extremely slow build times. When you use QEMU to compile code (e.g., running npm install or go build for ARM64 on an x86 machine), every instruction is emulated. For a large Node.js or Java application, this can take 10 times longer than a native build. If your build is timing out in CI/CD, consider using cross-compilation within your Dockerfile. For example, Go and Rust support cross-compilation natively. You can build the binary for ARM64 using the native x86 compiler and then simply copy the resulting binary into the final ARM64 layer.

Another common error involves APT or YUM repositories. If your Dockerfile adds third-party repositories, ensure they support both architectures. If you add a repository that only provides amd64 packages, the apt-get install step will fail when Buildx tries to execute it for the arm64 platform. Always use official base images like debian, alpine, or ubuntu, as these are natively multi-arch and provide the correct package lists automatically based on the platform context.

Optimization and Performance Tips

To achieve production-grade build speeds, you should implement Remote Caching. Buildx allows you to export your build cache to a registry or a local directory. By using --cache-from and --cache-to, subsequent builds only re-run steps that have changed. This is critical for multi-arch builds because it prevents re-emulating unchanged layers, which is the most time-consuming part of the process.

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --cache-from type=registry,ref=user/app:buildcache \
  --cache-to type=registry,ref=user/app:buildcache,mode=max \
  -t user/app:latest --push .

In a recent internal project, we reduced our CI pipeline duration from 45 minutes to 8 minutes just by switching to a type=gha cache (GitHub Actions specific) and utilizing native cross-compilation for our Golang binaries instead of relying on QEMU for the compilation phase. Whenever possible, perform the heavy lifting (compiling) on the native architecture of the build runner and only use Buildx to package the results into the multi-arch manifest.

Finally, utilize the BUILDPLATFORM and TARGETPLATFORM Dockerfile variables. These automatic arguments allow you to write conditional logic in your Dockerfile. For instance, you can download a specific version of a CLI tool depending on whether the target is ARM or AMD, ensuring your image remains lightweight and functional across all platforms.

📌 Key Takeaways

  • Multi-arch images use a Manifest List to point to architecture-specific digests.
  • Docker Buildx + QEMU enables building ARM64 images on AMD64 hardware.
  • Always use --push with multi-platform builds as the local Docker daemon has limited manifest support.
  • Optimize build times by using cross-compilation instead of full emulation for language-specific builds.

Frequently Asked Questions

Q. How do I check the architecture of a Docker image?

A. You can use the command docker image inspect <image-id> --format '{{.Architecture}}' for local images. For remote images, use docker buildx imagetools inspect <image-name> to see the full manifest list and supported architectures without pulling the whole image.

Q. Can I build multi-arch images without pushing to a registry?

A. Not directly with a single command for multiple platforms, because the default Docker image store cannot hold manifest lists. However, if you enable the "containerd image store" feature in Docker Desktop settings, you can use --load with multi-platform builds locally.

Q. Is QEMU emulation safe for production builds?

A. Yes, it is functionally safe and produces valid binaries. However, it is very slow. For performance-critical CI/CD pipelines, it is better to use native ARM64 runners (like AWS Graviton self-hosted runners) or cross-compilation within the Dockerfile to minimize emulation time.

For more details on advanced BuildKit features, refer to the official Docker Buildx documentation.

Post a Comment