Skip to content

Fix: Docker Multi-Platform Build Not Working — buildx Fails, Wrong Architecture, or QEMU Error

FixDevs ·

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 directory

Or 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 aarch64

Or QEMU-based cross-compilation fails mid-build:

#8 [linux/arm64 4/7] RUN npm install
exec /bin/sh: exec format error

Or pushing a multi-platform manifest to a registry fails:

ERROR: failed to push: unexpected status: 400 Bad Request

Why This Happens

Docker’s multi-platform builds have several prerequisites that aren’t set up by default:

  • docker build (classic builder) ignores --platform for cross-arch builds — only docker buildx with a builder that supports multiple platforms can actually build for a different architecture. The default docker build command 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-container driver) may only support a single platform. You need a builder created with the docker-container driver and QEMU available.
  • --load and --push are 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.

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 --bootstrap

Fix 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 entries

Multi-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=max

Fix 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=$BUILDPLATFORM tells 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  ← correct

Persistent 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 --persistent

Fix 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/digests

Still Not Working?

--load fails with multi-platformdocker 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.

For related Docker issues, see Fix: Docker Build Arg Not Available and Fix: Docker Multi-Stage Build Failed.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles