A docker buildx build --platform linux/amd64,linux/arm64 invocation aborts with ERROR: Multiple platforms feature is currently not supported for docker driver because the default builder cannot emit a multi-architecture manifest. This page walks through creating a docker-container buildx builder, wiring up QEMU emulation, and producing one image that serves both Intel and Apple Silicon teammates from a single machine; it is part of the cross-platform container build patterns within the wider containerized local environment patterns. If your rebuilds are also slow, pair this with optimizing Docker Compose for fast local rebuilds so the emulated arch does not double your feedback loop.

Diagnostic

The symptom is exact and reproducible: a build that succeeds for a single platform fails the moment you ask for two. Start by confirming which builder is active, because the default docker driver is the one that cannot do this. docker buildx ls lists every builder and marks the current one with an asterisk:

#!/usr/bin/env bash
set -euo pipefail
docker buildx ls
docker buildx inspect --bootstrap | grep -E 'Driver|Platforms'

On a stock Docker Desktop or Engine install you will see the built-in builder using the docker driver, and the Platforms line reports only your host architecture plus whatever the host can natively run. Now reproduce the failure directly — request two platforms against that default builder:

#!/usr/bin/env bash
set -euo pipefail
docker buildx build --platform linux/amd64,linux/arm64 -t demo:multi .

The BAD output is unambiguous and is the error this page resolves:

[+] Building 0.0s (0/0)
ERROR: Multiple platforms feature is currently not supported for docker driver.
Please switch to a different driver (eg. "docker buildx create --use")

A second, subtler failure hides even when the build starts. If you build for a single foreign platform without QEMU registered, the RUN steps fail as soon as they execute a binary for the wrong architecture:

#!/usr/bin/env bash
set -euo pipefail
docker buildx build --platform linux/arm64 -t demo:arm --load .
 => ERROR [2/4] RUN apt-get update
exec /bin/sh: exec format error

exec format error means the kernel tried to run an arm64 binary on an amd64 host with no emulation layer installed. Both of these — the driver error and the format error — must be cleared before a single command can emit both architectures.

From single Dockerfile to a two-architecture manifest One Dockerfile passes through a container-driver builder that fans out to an amd64 and an arm64 build, which merge into one manifest list. One Build, Two Architectures Dockerfile single source buildx builder container driver linux/amd64 linux/arm64 manifest list
A container-driver builder fans one Dockerfile out into two per-arch builds and merges them into a single manifest list.

Root cause

The default builder created by Docker uses the docker driver, which writes results straight into the local Engine image store. That store is a single-architecture store: a repository tag maps to exactly one image config, so there is nowhere for a manifest list (the index that points at both an amd64 and an arm64 image) to live. BuildKit therefore refuses the request up front rather than silently dropping an architecture. The fix is a different driver — docker-container — which runs BuildKit inside a dedicated container that can assemble and export an OCI manifest list, either to a registry or to a local tarball.

The exec format error is a separate prerequisite. Building an arm64 image on an amd64 host means every RUN step executes arm64 binaries. The host kernel cannot do that alone; it needs binfmt_misc handlers that route foreign binaries through QEMU user-mode emulation. Docker Desktop ships these handlers preconfigured, but a plain Linux Engine install does not, which is why the same command that works on a Mac fails on a CI-like Ubuntu host until you register QEMU. Once both pieces are in place — the container driver for the manifest and QEMU for the foreign instructions — one machine can produce every architecture your team runs.

It helps to be precise about the two distinct things buildx is combining here, because conflating them is what makes the error confusing. The first is the output format: a single-platform build produces one image config plus its layers, while a multi-platform build produces an OCI image index — a small JSON document that lists each platform and the digest of the per-platform image it maps to. The second is execution: producing the arm64 image still requires running arm64 code somewhere. The container driver solves the output-format problem, and QEMU solves the execution problem; neither substitutes for the other. A build can fail the driver check before it ever reaches a RUN step, and it can pass the driver check and then fail on exec format error the instant an emulated instruction runs, so treat the two fixes as an ordered pair rather than alternatives.

Resolution

  1. Register QEMU emulation handlers so the kernel can execute foreign-architecture binaries during RUN steps. This installs binfmt_misc entries for every architecture the image supports:

    #!/usr/bin/env bash
    set -euo pipefail
    docker run --privileged --rm tonistiigi/binfmt --install all
    docker buildx ls | grep -i 'linux/'
  2. Create a docker-container builder and select it. The --use flag makes it the active builder for subsequent commands; --bootstrap starts its BuildKit container immediately so the first real build does not pay the startup cost:

    #!/usr/bin/env bash
    set -euo pipefail
    docker buildx create \
      --name multiarch \
      --driver docker-container \
      --bootstrap --use
    docker buildx inspect multiarch | grep -E 'Name|Driver|Platforms'
  3. Write a Dockerfile that stays architecture-neutral. Let BuildKit inject the target platform rather than hardcoding a base image digest for one arch, and use the automatic TARGETPLATFORM build arg only where you genuinely need it:

    # Dockerfile
    FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
    ARG TARGETOS
    ARG TARGETARCH
    WORKDIR /src
    COPY go.* ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
        go build -o /out/app ./cmd/app
    
    FROM alpine:3.19
    COPY --from=build /out/app /usr/local/bin/app
    ENTRYPOINT ["app"]
  4. Build both platforms in one invocation. Pushing to a registry is the native way to store a manifest list, so tag for a real (or local) registry and push:

    #!/usr/bin/env bash
    set -euo pipefail
    docker buildx build \
      --builder multiarch \
      --platform linux/amd64,linux/arm64 \
      -t registry.local:5000/demo:multi \
      --push .
  5. Keep it in Compose so teammates build the same matrix without memorizing flags. Compose v2 reads a platforms list under build and drives buildx for you:

    # docker-compose.yml
    services:
      app:
        image: registry.local:5000/demo:multi
        build:
          context: .
          platforms:
            - linux/amd64
            - linux/arm64

    Run docker buildx bake or docker compose build with the multiarch builder selected. If you only need the image loaded into your local Engine for a quick run, drop to a single platform with --load, because --load cannot import a manifest list into the single-arch store.

docker driver versus docker-container driver A comparison of the default docker driver and the docker-container driver across output store, multi-platform support, and load behaviour. Which Driver Emits a Manifest List docker (default) Engine image store one arch per tag multi-platform: rejected --load works docker-container BuildKit container manifest list export multi-platform: yes --push works
Only the docker-container driver can assemble the manifest list that lets one tag serve both architectures.

Expected output

A correct multi-platform build streams two sets of steps — one per architecture — and finishes by exporting a manifest list. The tail of a successful --push build looks like this:

 => [linux/amd64 build 4/4] RUN go build -o /out/app ./cmd/app     6.1s
 => [linux/arm64 build 4/4] RUN go build -o /out/app ./cmd/app    41.8s
 => exporting to image
 => => exporting manifest list sha256:9c2f...:                     0.0s
 => => pushing layers                                              3.4s
 => => pushing manifest for registry.local:5000/demo:multi

The two build 4/4 lines confirm both architectures ran, and the exporting manifest list line confirms the index was assembled. Verify the published result independently with docker buildx imagetools, which reads the manifest list back from the registry and prints one entry per platform:

#!/usr/bin/env bash
set -euo pipefail
docker buildx imagetools inspect registry.local:5000/demo:multi
Name:      registry.local:5000/demo:multi
MediaType: application/vnd.oci.image.index.v1+json
Manifests:
  Platform:  linux/amd64
  Platform:  linux/arm64

Two Platform: lines under a single name is the definition of success: an Apple Silicon laptop pulling that tag gets the arm64 image, and an Intel CI runner pulling the identical tag gets the amd64 image, with no per-machine build required. The Docker client performs that selection automatically at pull time by matching the daemon's architecture against the index entries, so nothing downstream needs to know the image is multi-arch — docker run registry.local:5000/demo:multi just works on both hosts.

The emulated architecture is where build time concentrates, and the earlier chart makes the ratio concrete: the arm64 compile ran several times longer than its native amd64 counterpart on the same machine. That cost is real but bounded to the emulated half, so two levers keep it manageable. Ordering the Dockerfile so that expensive, arch-independent work (dependency downloads, module resolution) happens in a --platform=$BUILDPLATFORM stage means that work runs natively once instead of under emulation twice. Reserving the emulated path for the final compile and copy keeps the slow portion as small as the build allows, which is exactly the layer-ordering discipline that also governs fast local rebuilds.

Per-architecture build time, native versus emulated Bar chart comparing build seconds for the native amd64 build, the QEMU-emulated arm64 build, and a native arm64 build on Apple Silicon. Build Time by Target (seconds) amd64 native 18s arm64 (QEMU) 71s arm64 native 16s
Emulated arm64 compilation ran roughly four times slower than native; heavy compile steps are where QEMU cost concentrates.

Prevention

The two-architecture build works today; keeping it working means removing the ways it silently degrades to one architecture or breaks on a fresh machine.

  • Pin the builder in a bootstrap script. A new clone has no multiarch builder, so a teammate's first --platform build hits the driver error again. Add an idempotent setup step to your make bootstrap target that creates the builder only if it is missing: docker buildx inspect multiarch >/dev/null 2>&1 || docker buildx create --name multiarch --driver docker-container --bootstrap. This ties into writing a make bootstrap target for one-command setup.
  • Gate the manifest in CI. A build can succeed while quietly emitting one architecture if someone drops a --platform flag. Add a check that fails the pipeline when a platform is missing: pipe docker buildx imagetools inspect "$IMAGE" --raw through a jq assertion that both linux/amd64 and linux/arm64 appear in .manifests[].platform.
  • Keep base images multi-arch. A build breaks with no match for platform in manifest when a FROM image lacks an arm64 variant. Before adopting a base image, run docker buildx imagetools inspect <image> and confirm it lists every platform you target; prefer official images, which are almost always multi-arch. Watch for env drift between the arches too, since a variable set on one platform's base but not the other surfaces exactly like the leaks covered in debugging env variable leakage in multi-stage Docker builds.
Choosing native build versus QEMU emulation A decision on whether a native builder for the target architecture is available, leading to either a native node build or QEMU emulation. Native Node or QEMU? Have a native builder for the target arch? Yes append node, build natively No emulate via QEMU binfmt
When a native builder for the target architecture exists, append it as a builder node; otherwise fall back to QEMU emulation.

Platform caveats

macOS (Docker Desktop): QEMU handlers ship preinstalled, so you can skip the binfmt step, but emulated amd64 builds run through the Rosetta/QEMU layer and are slow — enable "Use Rosetta for x86/amd64 emulation" in Settings for a large speedup on Apple Silicon. WSL2: Register binfmt inside the WSL distro, not on Windows, and keep the build context on the Linux filesystem (~/, not /mnt/c); the docker-container builder reads context over the same 9p bridge that slows every cross-mount operation. Apple Silicon (ARM64): arm64 is your native platform, so only the amd64 half of the build is emulated. If your base image or a compiled dependency has no amd64 wheel, the emulated build is where it will surface first.

Rollback

#!/usr/bin/env bash
set -euo pipefail
docker buildx use default
docker buildx rm multiarch || true
docker buildx prune -f

Switching back to the default builder restores single-architecture behavior; removing the multiarch builder tears down its BuildKit container, and prune reclaims the build cache it accumulated.

Frequently Asked Questions

Why does --platform linux/amd64,linux/arm64 fail with the docker driver?

The default builder uses the docker driver, which writes into the local Engine image store. That store maps one tag to exactly one image config, so it has no place to keep a manifest list that points at two architectures. BuildKit rejects the request rather than dropping an arch. Create a docker-container builder with docker buildx create --driver docker-container --use, which runs BuildKit in a container that can export an OCI manifest list.

Can I load a multi-arch image into my local Docker with --load?

No. --load imports the result into the single-architecture Engine store, which cannot hold a manifest list, so a two-platform build with --load fails. Use --push to a registry (a local registry:2 works fine) to store the full manifest list, or build a single platform with --load when you only need to run the image locally on your host architecture.

Do I need QEMU if I only build for my own architecture?

No. QEMU is only needed to execute foreign-architecture binaries during RUN steps. If every target platform is one your host can run natively, the exec format error never appears. The moment you add a foreign platform — arm64 on an Intel host or amd64 on Apple Silicon — install the handlers with docker run --privileged --rm tonistiigi/binfmt --install all so the kernel can route those binaries through emulation.

How do I confirm both architectures actually made it into the published image?

Run docker buildx imagetools inspect <image> and read the Manifests section. A correct multi-arch image is an image index whose entries list each Platform: you built, such as linux/amd64 and linux/arm64. For a scriptable CI gate, add --raw and pipe the JSON through jq to assert that every required platform appears in .manifests[].platform.