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 KarunaratneIshan Karunaratne⏱️ 6 min readUpdated
Share thisCopied

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

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

d

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.