Fix: Docker Multi-Platform Build Not Working — buildx Fails, Wrong Architecture, or QEMU Error
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Docker multi-platform build issues — buildx setup, QEMU registration, --platform flag usage, architecture-specific dependencies, and pushing multi-arch manifests to a registry.
The Problem
docker buildx build --platform linux/arm64 fails immediately:
ERROR: failed to solve: failed to read dockerfile: no such file or directoryOr the build runs but the image is the wrong architecture:
docker build --platform linux/arm64 -t myimage .
docker run --rm myimage uname -m
# x86_64 — expected aarch64Or QEMU-based cross-compilation fails mid-build:
#8 [linux/arm64 4/7] RUN npm install
exec /bin/sh: exec format errorOr pushing a multi-platform manifest to a registry fails:
ERROR: failed to push: unexpected status: 400 Bad RequestWhy This Happens
Docker’s multi-platform builds have several prerequisites that aren’t set up by default:
docker build(classic builder) ignores--platformfor cross-arch builds — onlydocker buildxwith a builder that supports multiple platforms can actually build for a different architecture. The defaultdocker buildcommand may silently build for the host architecture.- QEMU must be registered as a binfmt handler — to run non-native binaries (e.g., ARM64 binaries on an x86_64 host), the kernel’s binfmt_misc must map ARM64 executables to QEMU. Without this, you get
exec format error. - The builder instance must be multi-platform-capable — the default buildx builder (
docker-containerdriver) may only support a single platform. You need a builder created with thedocker-containerdriver and QEMU available. --loadand--pushare mutually exclusive with multi-platform —--load(load image into local Docker) only works for single-platform builds. Multi-platform builds must be pushed directly to a registry with--push, or exported with--output.
The deeper reason this confuses people is that Docker has two completely different build engines coexisting on the same CLI. The classic builder (sometimes called “legacy” or “Docker build v0”) is the original implementation that ships with the Docker daemon. It reads Dockerfile, walks each instruction sequentially, and produces a single-platform image. BuildKit is a re-architected build engine introduced in 2018 and exposed through docker buildx from 2019. It runs builds inside a containerized “builder instance,” supports parallel execution, mounts, secrets, cache exporters, and — relevant here — multi-platform output via a manifest list. When you type docker build, the daemon chooses whichever engine is configured. When you type docker buildx build, BuildKit is mandatory. Almost every “the --platform flag did nothing” complaint comes from running docker build against the classic engine on a host where BuildKit wasn’t made the default.
The picture changed materially with Docker Engine 23.0 (Feb 2023), which made BuildKit the default builder for both docker build and docker buildx build on Linux. Before 23.0, you had to opt in with DOCKER_BUILDKIT=1 or by setting "features": { "buildkit": true } in /etc/docker/daemon.json. Documentation, tutorials, and CI templates from 2019-2022 still assume the classic builder is the default and explicitly export DOCKER_BUILDKIT=1. On a modern Docker installation that environment variable is redundant — but harmless. The version cutover also moved the build cache format and changed how --load interacts with multi-platform builds, which is why old GitHub Actions workflows often need updates after the runner image refreshes.
The third layer is the driver that backs your buildx builder. Buildx supports four drivers: docker (uses the local daemon, single-platform only, no concurrent builds), docker-container (spins up a BuildKit container, supports multi-platform with QEMU, the default for buildx create), kubernetes (runs BuildKit in a Pod, useful for shared CI clusters), and remote (connects to a remote BuildKit daemon over TCP). If docker buildx ls shows your active builder as default with driver docker, you cannot build multi-platform images no matter how many --platform flags you pass. You must create a docker-container builder, and that builder must have QEMU available so it can execute non-native binaries during build steps.
Fix 1: Set Up buildx and QEMU Correctly
Install and configure the prerequisites for multi-platform builds:
# Step 1: Verify buildx is available
docker buildx version
# buildx v0.12.0 docker-desktop ← OK
# If not found: install Docker Desktop or update Docker Engine
# Step 2: Register QEMU binfmt handlers (Linux hosts only)
# Docker Desktop on Mac/Windows does this automatically
docker run --privileged --rm tonistiigi/binfmt --install all
# Installs QEMU for arm, arm64, riscv64, ppc64le, s390x, mips, mips64
# Verify QEMU registration
ls /proc/sys/fs/binfmt_misc/
# Should show: qemu-aarch64, qemu-arm, etc.
# Step 3: Create a new builder with multi-platform support
docker buildx create --name multiarch-builder --driver docker-container --use
docker buildx inspect --bootstrap
# Should show: Platforms: linux/amd64, linux/arm64, linux/arm/v7, ...Verify the builder can handle your target platform:
docker buildx ls
# NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
# multiarch-builder * docker-container running linux/amd64, linux/arm64, linux/arm/v7
# If your platform is missing, rebuild with QEMU registered first
docker buildx rm multiarch-builder
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name multiarch-builder --driver docker-container --use
docker buildx inspect --bootstrapFix 2: Build and Push Multi-Platform Images
The correct workflow for multi-platform images:
# Build for multiple platforms and push to registry in one step
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myregistry/myimage:latest \
--push \
.
# Build for a single non-native platform (for testing)
docker buildx build \
--platform linux/arm64 \
--tag myimage:arm64-test \
--load \ # Load into local Docker daemon (single platform only)
.
# Build and export to local tar file
docker buildx build \
--platform linux/arm64 \
--output type=oci,dest=myimage-arm64.tar \
.
# Verify the manifest has multiple architectures
docker buildx imagetools inspect myregistry/myimage:latest
# Outputs manifest list with amd64 and arm64 entriesMulti-platform build in GitHub Actions:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Installs QEMU binfmt handlers automatically
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Creates a multi-platform capable builder
- name: Log in to 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@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=maxFix 3: Handle Architecture-Specific Dependencies
Some dependencies install different binaries per architecture. Handle this in the Dockerfile:
# Use ARG TARGETARCH — automatically set by buildx to the target platform
FROM node:20-alpine
ARG TARGETARCH
ARG TARGETOS
# Install architecture-specific binary
RUN case "${TARGETARCH}" in \
"amd64") ARCH="x64" ;; \
"arm64") ARCH="arm64" ;; \
"arm") ARCH="armv7" ;; \
*) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \
esac && \
wget -O /usr/local/bin/mybin "https://releases.example.com/mybin-linux-${ARCH}" && \
chmod +x /usr/local/bin/mybin
COPY . .
RUN npm ci
CMD ["node", "server.js"]Use --platform=$BUILDPLATFORM for build-time tools:
# syntax=docker/dockerfile:1
# Build stage runs on the host platform (fast, no emulation)
FROM --platform=$BUILDPLATFORM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # Runs natively on host — much faster than under QEMU
COPY . .
RUN npm run build
# Runtime stage runs on the target platform
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]Note:
--platform=$BUILDPLATFORMtells Docker to run that stage on the host platform (e.g., amd64) even when building for arm64. This avoids QEMU emulation for slow build tools like compilers and package managers.
Cross-compile CGO Go binaries:
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO_ENABLED=0 for static binary; set GOOS/GOARCH for cross-compilation
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -ldflags="-s -w" -o /app/server .
FROM --platform=$TARGETPLATFORM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]Fix 4: Fix QEMU exec format error
exec format error during a build step means QEMU isn’t handling the binary format:
# Check QEMU is installed and binfmt is registered
cat /proc/sys/fs/binfmt_misc/qemu-aarch64
# enabled
# interpreter /usr/bin/qemu-aarch64-static
# flags: OCF
# ...
# If qemu-aarch64 doesn't exist, re-register:
docker run --privileged --rm tonistiigi/binfmt --install arm64
# Verify QEMU is working by running an ARM64 container
docker run --rm --platform linux/arm64 alpine uname -m
# aarch64 ← correctPersistent QEMU registration across reboots (Linux):
# Install qemu-user-static package (registers permanently)
sudo apt-get install -y qemu-user-static
sudo systemctl restart systemd-binfmt
# Or use the tonistiigi/binfmt image with --persistent flag
docker run --privileged --rm tonistiigi/binfmt --install all --persistentFix 5: Cache Multi-Platform Builds
Multi-platform builds are slow without caching. Use registry-based caching:
# Push cache to registry (works across machines and CI runs)
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=myregistry/myimage:buildcache \
--cache-to type=registry,ref=myregistry/myimage:buildcache,mode=max \
--tag myregistry/myimage:latest \
--push \
.
# GitHub Actions cache (for private repos or avoiding registry costs)
docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
...Build matrix strategy — native builds for each arch (faster than QEMU):
# GitHub Actions: build natively on arm64 and amd64 runners, then merge
jobs:
build:
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm # GitHub's ARM64 runner
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
path: /tmp/digests/*
merge:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: digests-*
merge-multiple: true
path: /tmp/digests
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository }}:latest \
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
working-directory: /tmp/digestsVersion History: BuildKit, buildx, and Multi-Platform Builds
The order in which features landed in Docker explains why so many tutorials contradict each other on multi-platform builds.
Docker 18.06 (mid-2018) — BuildKit experimental. Shipped behind DOCKER_BUILDKIT=1. The classic builder was still the default, no multi-platform support yet.
Docker 19.03 (July 2019) — docker buildx plugin. The buildx CLI plugin shipped as the official BuildKit frontend. Introduced multi-platform builds via manifest-list output, the docker-container driver, and QEMU binfmt_misc integration. Most “modern” multi-platform tutorials were written against this version.
Docker 20.10 (Dec 2020) — buildx as a first-class command. Apple Silicon (M1) launched and turned multi-platform builds from a niche concern into a daily problem — Dockerfiles written on a Mac would not run on Linux servers without explicit --platform linux/amd64 flags.
Docker 22.06 (mid-2022) — BuildKit 0.10. Added SLSA-style build attestations and SBOM generation. Cache mounts in Dockerfiles became stable (RUN --mount=type=cache,target=/root/.cache), critical for fast multi-platform builds because each architecture needs its own cache slice.
Docker 23.0 (Feb 2023) — BuildKit is the default. The watershed release. docker build now uses BuildKit by default on Linux, so DOCKER_BUILDKIT=1 is no longer required. The classic builder is still available via DOCKER_BUILDKIT=0 for compatibility but is end-of-life. Multi-platform builds with --platform linux/amd64,linux/arm64 work without any opt-in environment variable, assuming a multi-platform-capable builder is in use.
Docker 24.0 (June 2023) — Buildx 0.11, containerd image store opt-in. The new containerd image store (still opt-in via Docker Desktop settings) lets docker images and docker load handle manifest lists directly, removing the historical restriction that --load was single-platform only when the daemon used the classic image store.
Docker 25.0 (Jan 2024) — containerd image store stable. Multi-platform --load finally works without registry round-trips when the containerd image store is enabled. The default for new Docker Desktop installs flipped to containerd in 2024-2025.
Docker 26.0 / 27.0 (2024) — Buildx 0.13/0.14. Better cache management, faster imagetools operations. GitHub Actions’ ubuntu-24.04-arm runners (GA 2024) removed the QEMU emulation penalty for ARM64 builds.
QEMU and binfmt_misc. The kernel’s binfmt_misc module is what makes docker run --platform linux/arm64 alpine work on an x86_64 host: it maps the ARM64 ELF magic number to /usr/bin/qemu-aarch64-static. The tonistiigi/binfmt image registers these mappings for every architecture (arm, arm64, riscv64, ppc64le, s390x, mips, mips64). Without it, the kernel sees an ELF it can’t execute and returns exec format error. Docker Desktop on macOS and Windows registers these handlers automatically (because the VM is always Linux-on-virtualization), but bare-metal Linux hosts and self-hosted CI runners must register them manually. The --persistent flag tells tonistiigi/binfmt to write the registrations in a form that survives reboots.
Still Not Working?
--load fails with multi-platform — docker buildx build --load only supports a single platform. When building for linux/amd64,linux/arm64, you must use --push to a registry or --output type=oci,dest=archive.tar. To test locally, build for a single platform first: --platform linux/arm64 --load.
Python packages fail to compile for ARM64 under QEMU — packages that compile C extensions (like numpy, Pillow, cryptography) can take 10-100x longer under QEMU emulation compared to native. Use pre-built wheels when available (pip install --only-binary=:all: package) or use the --platform=$BUILDPLATFORM technique with cross-compilation.
Registry doesn’t support manifest lists — older registries (including some self-hosted Harbor instances) may not support OCI manifest lists. Update your registry to a version that supports the OCI Image Index specification, or push each architecture separately with different tags.
Alpine-based images failing for ARM — some Alpine packages have ARM-specific issues. If you see Bus error or Illegal instruction during package installation, try using a Debian-based image (-slim variants) instead. ARM support in Alpine improved significantly after Alpine 3.17.
exec /bin/sh: exec format error only on the second-stage image — your build stage uses FROM --platform=$BUILDPLATFORM (host arch) but a later stage doesn’t, and you copied a binary from the build stage into a target-arch runtime stage. The host-built binary then fails to execute under QEMU. Fix by cross-compiling in the build stage (GOOS/GOARCH for Go, cargo build --target=... for Rust) so the binary already matches the target architecture, not the build host.
buildx cache not invalidating after upgrading the base image — the cache-from/cache-to registry cache is keyed by both the architecture and the BuildKit version. A cache built on BuildKit 0.12 may not be reused by BuildKit 0.14 for some instructions. Use cache-to=...,mode=max (writes all layers, not just the final stage) and pin BuildKit versions in CI with docker/setup-buildx-action@v3 with: version: v0.14.0 to keep caches stable.
failed to compute cache key with no useful detail — almost always a missing file referenced by a COPY instruction, but the BuildKit error is opaque. Re-run the build with BUILDKIT_PROGRESS=plain (or --progress=plain) to see the full per-line output. If you are calling buildx from a shell with set -e, a single missing file in a multi-stage build can also surface as this message — check that the parent script isn’t swallowing the real error.
For related Docker issues, see Fix: Docker Build Arg Not Available, Fix: Docker Multi-Stage Build Failed, Fix: Docker exec format error, and Fix: Docker Layer Cache Invalidated.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: GitHub Actions Docker Build and Push Failing
How to fix GitHub Actions Docker build and push errors — registry authentication, image tagging, layer caching, multi-platform builds, and GHCR vs Docker Hub setup.
Fix: AWS ECR Authentication Failed (docker login and push Errors)
How to fix AWS ECR authentication errors — no basic auth credentials, token expired, permission denied on push, and how to authenticate correctly from CI/CD pipelines and local development.
Fix: Coolify Not Working — Deployment Failing, SSL Not Working, or Containers Not Starting
How to fix Coolify self-hosted PaaS issues — server setup, application deployment, Docker and Nixpacks builds, environment variables, SSL certificates, database provisioning, and GitHub integration.
Fix: Docker Secrets Not Working — BuildKit --secret Not Mounting, Compose Secrets Undefined, or Secret Leaking into Image
How to fix Docker secrets — BuildKit secret mounts in Dockerfile, docker-compose secrets config, runtime vs build-time secrets, environment variable alternatives, and verifying secrets don't leak into image layers.