← Back to DevBytes

Docker Multi-Architecture Builds: Production Guide

Understanding Multi-Architecture Container Images

A multi-architecture container image is a single image reference (like myapp:latest) that serves different binary versions of your application depending on the host’s CPU architecture. Docker achieves this through manifest lists (also called OCI image indexes). When you pull an image, the Docker client sends its own OS and architecture details, and the registry returns the correct manifest for that platform. This means the same docker run command works on an Intel-based cloud VM, an AWS Graviton ARM instance, or a Raspberry Pi at the edge — all without changing tags or manually selecting the right variant.

Why Multi-Architecture Builds Matter in Production

🚀 Deploy your AI agent in 10 minutes

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

Try it free →

Modern production environments are increasingly heterogeneous. You might run:

Without multi-architecture images, you’d need separate tags per architecture (myapp:1.0-amd64, myapp:1.0-arm64) and complex orchestration logic to select the correct tag. This breaks GitOps workflows, complicates Helm charts, and introduces risk of mismatched deployments. With a properly built manifest list, one tag fits all, eliminating exec format error failures and simplifying rollouts. It also ensures consistency across platforms, because the same manifest list digest represents the same logical release regardless of architecture.

Core Concepts: Manifests and Image Indexes

A traditional Docker image consists of a manifest that references a config file and a set of layers. A manifest list (or OCI image index) is a JSON document that contains pointers to several such manifests, each annotated with platform information (OS, architecture, variant). When you run docker pull myimage:latest, the registry checks the manifest list, matches the requesting client’s platform, and serves the appropriate manifest. The client then downloads only the layers for that specific architecture. The manifest list itself has a digest; signing and attestation can be applied at this level for supply chain security.

Prerequisites and Setup

To build multi-architecture images you need:

Enabling buildx and QEMU

First, ensure you have a buildx builder instance that supports multiple platforms. You can create one with:

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

The --bootstrap flag starts the builder container and ensures it’s ready. Next, register QEMU handlers in the kernel so the builder can emulate foreign architectures. The easiest way is:

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

This runs a privileged container that writes the appropriate binfmt_misc entries for architectures like arm64, arm/v7, riscv64, etc. After this, your builder can run binaries for other architectures transparently using QEMU. Docker Desktop already sets up these handlers automatically when you enable “Use containerd for pulling and storing images” (settings > General), but the manual step is useful on Linux servers.

Step-by-Step: Building Multi-Architecture Images

With the builder ready, you can build an image for multiple platforms in one command. Here is a practical example using a simple Go application.

Sample Dockerfile (Go, multi-stage)

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o server .

FROM alpine:3.20
COPY --from=build /app/server /usr/local/bin/server
EXPOSE 8080
CMD ["server"]

The --platform=$BUILDPLATFORM tells Docker to use the builder’s native platform for the build stage (faster cross-compilation with Go, since Go can cross-compile without emulation). The TARGETOS and TARGETARCH arguments are automatically provided by buildx and allow you to compile the correct binary.

Building and pushing the manifest list

Run the build command specifying the target platforms and the registry reference. Use --push to push the manifest list and all architecture-specific images directly to the registry.

docker buildx build \
  --platform linux/amd64,linux/arm64,linux/arm/v7 \
  -t myregistry/myapp:1.0 \
  --push \
  .

This command builds three separate images (amd64, arm64, arm/v7), pushes each, and then creates and pushes a manifest list tagged myregistry/myapp:1.0. If you omit --push and use --load, you’ll get an error because --load only supports loading a single-platform image into the local Docker engine. For local testing, you can build one platform at a time with --load --platform linux/arm64.

You can also output the final image to an OCI layout directory on disk:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myapp:1.0 \
  --output type=oci,dest=./oci-output \
  .

This directory contains the OCI image index and all architecture-specific blobs, which you can then push with tools like skopeo or use for offline transfer.

Using QEMU Emulation vs Native Builders

There are two strategies to handle building for multiple architectures:

Creating a multi-node native builder

Assume you have an amd64 node (local) and an arm64 node reachable at arm-node.local. First, set up SSH access and then create the builder:

docker buildx create \
  --name native-multi \
  --platform linux/amd64 \
  --driver docker-container \
  --use

# Add the remote arm64 node
docker buildx create \
  --append \
  --name native-multi \
  --platform linux/arm64 \
  --driver docker-container \
  --buildkitd-flags '--allow-insecure-entitlement' \
  tcp://arm-node.local:2375

Then docker buildx use native-multi. Now a build with --platform linux/amd64,linux/arm64 will compile natively on each machine. This is significantly faster for languages that don’t cross-compile well (like Python with native extensions, or C++ with heavy dependencies). However, for Go, Rust, and static cross-compilation setups, QEMU emulation with $BUILDPLATFORM is usually sufficient and simpler.

Building in CI/CD Pipelines

Multi-architecture builds fit naturally into modern CI/CD. Below is a GitHub Actions workflow example that builds and pushes a multi-arch image on every tag.

name: Build Multi-Arch Image

on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          push: true
          tags: ghcr.io/${{ github.repository }}:latest, ghcr.io/${{ github.repository }}:${{ github.ref_name }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The workflow uses setup-qemu-action to register binfmt handlers, setup-buildx-action to create a builder, and build-push-action to handle the multi-platform build and push. The cache-from and cache-to directives use GitHub Actions cache to speed up subsequent builds by sharing layer caches across runs.

For GitLab CI, you can use the Docker executor with buildx and QEMU registered in the runner’s environment, or use docker/buildx in a custom image. The key is to ensure binfmt is active and the builder supports the desired platforms.

Best Practices for Production

Verifying Multi-Arch Images

After pushing, you can inspect the manifest list to ensure it contains the expected platforms.

docker buildx imagetools inspect myregistry/myapp:1.0

Output shows each platform entry with its own digest and size. You can also use the classic manifest command:

docker manifest inspect myregistry/myapp:1.0

To test locally, pull and run an architecture-specific variant by forcing the platform:

docker run --rm --platform linux/arm64 myregistry/myapp:1.0 uname -m
# Outputs: aarch64

This confirms the correct binary is served.

Troubleshooting Common Issues

Conclusion

Multi-architecture container images have moved from a niche technique to a production necessity as ARM and RISC-V hardware proliferates in datacenters and edge environments. Docker Buildx makes the process straightforward — a single command can build for all platforms, and CI integrations automate the entire pipeline. By designing your Dockerfiles for cross-compilation, leveraging QEMU or native builders appropriately, and following tagging and verification best practices, you can ship a single image tag that works reliably everywhere. This reduces operational complexity, avoids architecture-specific errors, and keeps your deployments consistent across every node in your fleet.

🚀 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