TechEarl

docker buildx: Multi-Architecture (arm64 + amd64) Builds

Build a single image that runs on both Apple Silicon and Intel servers. Set up a buildx builder, register QEMU emulation, and push a multi-platform manifest list. With the --push vs --load gotcha.

Ishan Karunaratne⏱️ 6 min readUpdated
Share thisCopied
Build one Docker image that runs on Apple Silicon (arm64) and Intel/AMD (amd64). Set up buildx, install QEMU binfmt, and push a multi-platform manifest. Plus the --push vs --load trap.

If you build images on an Apple Silicon Mac and push them to an x86_64 Linux server, the obvious approach breaks: a default docker build produces an arm64 image, and the server can't run it. The fix is a multi-architecture image — a single tag in the registry that points at multiple per-architecture builds, with the registry handing out the right one to each puller. docker buildx is the tool that makes them.

The one-line version

bash
docker buildx create --use --name multiarch
docker buildx build --platform linux/amd64,linux/arm64 -t USERNAME/IMAGE:TAG --push .

That builds your Dockerfile for both architectures, packages them into a manifest list, and pushes to the registry. Anyone pulling the tag gets the build for their CPU automatically.

What buildx actually does

docker buildx is a Docker CLI plugin that exposes BuildKit's more advanced features, including building for multiple platforms at once. It uses a separate "builder instance" (not the default Docker builder) and can run builds either:

  • Natively for the host's CPU (fast).
  • Under QEMU emulation for foreign architectures (slow, but works).
  • On a remote build node with the matching CPU (fast, requires setup).

For most "build on Mac, deploy to Linux x86" cases, the workflow is: use buildx + QEMU emulation, accept the slower amd64 build, push the multi-arch result to a registry.

Setup: create a buildx builder

By default, Docker uses a builder named default that doesn't support multi-arch. You need a new builder:

bash
docker buildx create --use --name multiarch

That creates a builder called multiarch and switches to it. Check:

bash
docker buildx ls
# NAME/NODE       DRIVER/ENDPOINT  STATUS   PLATFORMS
# multiarch *     docker-container
#   multiarch0    desktop-linux    running  linux/amd64, linux/arm64, ...
# default         docker
#   default       default          running  linux/arm64

multiarch is now your active builder. Switching back to default:

bash
docker buildx use default

Register QEMU emulation (for cross-arch builds)

If you're on Apple Silicon building for amd64 (or vice versa), QEMU needs to be set up to emulate the foreign architecture. Docker Desktop usually does this automatically; if not:

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

That registers binfmt_misc handlers for every architecture binfmt supports. Check what's registered:

bash
docker run --privileged --rm tonistiigi/binfmt

Apple Silicon Macs with Docker Desktop usually have this set up out of the box.

Build for two platforms

bash
docker buildx build --platform linux/amd64,linux/arm64 \
  -t USERNAME/IMAGE:TAG \
  --push .

Two key flags:

  • --platform — comma-separated list of platforms. Each is os/arch[/variant]. linux/amd64 and linux/arm64 are the two you'll use 95% of the time.
  • --push — push the result to the registry. For multi-arch builds this is essentially required.

--push vs --load — the gotcha

bash
# Single-arch build to your local Docker image store
docker buildx build --platform linux/amd64 -t my-image --load .

# Multi-arch build pushed to a registry
docker buildx build --platform linux/amd64,linux/arm64 -t my-image --push .

# Multi-arch build to your LOCAL image store — DOES NOT WORK
docker buildx build --platform linux/amd64,linux/arm64 -t my-image --load .
# Error: docker exporter does not currently support exporting manifest lists

The local Docker image store represents one image per tag. A multi-arch image is multiple images under one tag (a "manifest list"). The local store can't represent that, so multi-arch builds must export somewhere that can — either a registry (--push) or an OCI tarball (--output type=oci,dest=...).

The pragmatic workflow: push to a registry (Docker Hub, GHCR, your private registry), or run buildx for just one platform with --load when you want to test locally.

Building for a registry that requires authentication

bash
# Log in first
docker login                          # Docker Hub
docker login ghcr.io                  # GitHub Container Registry
docker login registry.example.com    # private registry

# Then build and push
docker buildx build --platform linux/amd64,linux/arm64 \
  -t ghcr.io/your-org/my-image:v1 \
  --push .

docker login stores credentials buildx then uses for the push.

Building in CI

GitHub Actions has a published action for this:

yaml
# .github/workflows/build.yml
name: Build multi-arch image
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

GitLab, CircleCI, and others have similar patterns — the core is setting up QEMU, configuring buildx, and using the right registry credentials.

Verify the manifest

After pushing, confirm both architectures are in the registry:

bash
docker manifest inspect USERNAME/IMAGE:TAG

You should see entries for linux/amd64 and linux/arm64 (and any others you built). The platform field on each blob is what Docker matches against when someone pulls.

Caching across architecture builds

Multi-arch builds are slow because emulated builds run 5-10x slower than native. Build cache helps a lot:

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

BuildKit pushes intermediate cache layers to the registry tagged as :cache, and pulls them on subsequent builds. The second build of an unchanged Dockerfile takes seconds instead of minutes.

Common pitfalls

  • docker build instead of docker buildx build. The default builder doesn't support multi-platform. Always use buildx for cross-arch.
  • Trying --load on a multi-arch build. Doesn't work. Either build single-arch and load, or build multi-arch and push.
  • No QEMU registered. Cross-arch builds fail with "exec format error" on the build commands themselves. Run docker run --privileged --rm tonistiigi/binfmt --install all once.
  • Slow builds. Emulation is slow. For frequent multi-arch builds, consider:
    • Native build nodes — buildx can use a remote arm64 server alongside your amd64 host so each arch builds natively.
    • CI matrix builds — let your CI builders provide native arm64 (GitHub Actions has arm64 runners; AWS CodeBuild has arm64 capacity).
  • Pushing to a registry that doesn't support OCI manifest lists. Modern Docker Hub, GHCR, GCR, ECR all do. Some older self-hosted registries (Harbor < 2.x, Nexus < specific versions) don't. Upgrade the registry.

What to do next

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsDockerbuildxMulti-Archarm64amd64BuildKitDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

View Docker container output with docker logs. Covers --follow, --tail, --since, --timestamps, log rotation, and what to do when the app writes to a file instead of stdout.

docker logs: View Container Output and Tail Logs

Read the stdout and stderr of a running or stopped container. Follow live output, tail the last N lines, filter by time, prepend timestamps, and the cases where docker logs doesn't help because the app writes to a file instead.