How to Fix Docker Buildx AMD64 Build Failures on ARM64 Hosts (QEMU binfmt_misc Error)
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke:
docker buildxcannot execute AMD64 binaries on your ARM64 host because the Linux kernel'sbinfmt_miscsubsystem has no registered QEMU interpreter forx86_64, or the registered binary is stale/wrong. - How to fix it: Re-register QEMU static binaries using the
tonistiigi/binfmtprivileged container, then recreate your buildx builder instance. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your failing
docker buildxcommand or Dockerfile and get a corrected setup script instantly.
The Incident (What does the error mean?)
Raw error output from the failing build:
$ docker buildx build --platform linux/amd64 -t myapp:latest .
[+] Building 0.0s (0/0)
error: failed to solve: failed to read dockerfile: failed to create LLB definition:
exec /bin/sh: exec format error
# OR the variant:
ERROR: failed to solve: no match for platform in manifest: not found
# OR seen in buildkitd logs:
failed to register binfmt_misc: exit status 1
Immediate consequence: Your CI pipeline or local cross-platform build is completely dead. Any multi-arch image build targeting linux/amd64 from an Apple Silicon Mac, AWS Graviton, or any ARM64 Linux host will fail at the first RUN instruction that executes an AMD64 binary — or fail before even starting if the builder manifest can't resolve the platform.
The Attack Vector / Blast Radius
This is not a security vulnerability — it is a hard infrastructure failure with a wide blast radius in multi-arch CI environments:
- Apple Silicon developers (M1/M2/M3) hitting this locally block every other engineer who pulls their broken image layer cache.
- Graviton-based CI runners (GitHub Actions
ubuntu-22.04-arm, self-hosted AWS Graviton) will silently fail ifbinfmt_miscwas never initialized in the runner's Docker-in-Docker (DinD) sidecar — this is the most common production surprise. - Docker Desktop on macOS ARM ships QEMU internally but the
binfmt_miscregistrations live inside the LinuxKit VM. A Docker Desktop update or VM reset can wipe them without warning. - Cascading failure: If your base image (
FROM node:18) resolves to an AMD64 manifest and your builder has no emulation, every single layer pull and RUN exec fails, not just the final step. BuildKit will report misleading cache errors that mask the real cause. - The silent killer:
docker buildx lsmay show your builder asrunningand even listlinux/amd64as a supported platform based on a stale kernel registration — the failure only surfaces at build time.
How to Fix It (The Solution)
Step 0 — Confirm the actual failure mode
# Check what binfmt_misc handlers are currently registered
ls /proc/sys/fs/binfmt_misc/
# You should see: qemu-x86_64 (among others)
# If you only see: register status — QEMU is NOT registered
# Check if the registered interpreter binary actually exists
cat /proc/sys/fs/binfmt_misc/qemu-x86_64
# Look for the 'interpreter' line — verify that path exists on disk
Basic Fix — Re-register QEMU via tonistiigi/binfmt
This is the canonical, Docker-blessed approach. Run this once per host boot (or add it to your runner startup script):
- # Nothing registered, or stale registration from a manual apt install of qemu-user-static
- apt-get install -y qemu-user-static
- update-binfmts --enable
+ # Correct approach: use the official multi-arch QEMU image
+ docker run --privileged --rm tonistiigi/binfmt --install all
+
+ # Verify registration succeeded
+ docker buildx inspect --bootstrap
+ # Confirm linux/amd64 appears in the Platforms list
Why tonistiigi/binfmt and not apt? The apt package installs QEMU binaries but does not guarantee the F (fix-binary) flag is set in the binfmt_misc registration. Without F, the interpreter path is resolved at exec time inside the container's mount namespace — where it doesn't exist. The tonistiigi/binfmt image sets the F flag, making the registration namespace-agnostic.
Enterprise Best Practice — Idempotent builder bootstrap in CI
For GitHub Actions, GitLab CI, and Jenkins on ARM64 runners, never assume QEMU state. Encode the full bootstrap sequence:
- # Naive CI setup that breaks on fresh Graviton runners
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ # Correct: explicit QEMU registration before buildx setup
+ - name: Register QEMU binfmt handlers
+ run: |
+ docker run --privileged --rm tonistiigi/binfmt --install all
+ # Validate — fail fast if amd64 emulation is missing
+ grep -q 'qemu-x86_64' /proc/sys/fs/binfmt_misc/ \
+ || (echo 'FATAL: qemu-x86_64 binfmt not registered' && exit 1)
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ with:
+ # Force a fresh builder — never reuse a builder with stale platform cache
+ driver-opts: image=moby/buildkit:buildx-stable-1
+ install: true
+ cleanup: true
For Docker Compose / DinD environments (e.g., GitLab CI with services: docker:dind):
- services:
- - docker:dind
+ services:
+ - name: docker:dind
+ # DinD must run privileged for binfmt_misc writes to the host kernel
+ command: ["--experimental"]
+
+ before_script:
+ - docker run --privileged --rm tonistiigi/binfmt --install all
+ - docker buildx create --use --name multiarch-builder --driver docker-container
+ - docker buildx inspect --bootstrap
Critical: DinD containers that are not run with --privileged cannot write to /proc/sys/fs/binfmt_misc. This is a kernel namespace restriction — there is no workaround except granting the privilege or using a pre-registered host.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Add a platform smoke-test gate before every multi-arch build job
#!/bin/bash
# scripts/verify-qemu.sh — run this as a preflight check
set -euo pipefail
REQUIRED_PLATFORMS=("qemu-x86_64" "qemu-aarch64" "qemu-arm")
for platform in "${REQUIRED_PLATFORMS[@]}"; do
if [[ ! -f "/proc/sys/fs/binfmt_misc/${platform}" ]]; then
echo "ERROR: binfmt handler missing: ${platform}"
echo "Run: docker run --privileged --rm tonistiigi/binfmt --install all"
exit 1
fi
done
echo "All QEMU binfmt handlers verified."
2. Pin the tonistiigi/binfmt image digest in production
Floating tags (--install all pulling latest) introduce supply-chain risk. Pin to a digest:
# Get the current digest
docker pull tonistiigi/binfmt:latest
docker inspect tonistiigi/binfmt:latest --format '{{index .RepoDigests 0}}'
# Use digest in CI
docker run --privileged --rm \
tonistiigi/binfmt@sha256:<YOUR_PINNED_DIGEST> \
--install all
3. GitHub Actions: Use docker/setup-qemu-action as the canonical solution
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: amd64,arm64
# Pin the action version in your dependabot config
This action wraps tonistiigi/binfmt and is maintained by Docker, Inc. It is the least-surprise option for GitHub-hosted runners.
4. Checkov / OPA policy — flag missing QEMU setup in pipeline definitions
If you use Checkov for CI config scanning, write a custom check that fails any workflow YAML containing --platform linux/amd64 on an ARM runner without a preceding setup-qemu-action or tonistiigi/binfmt step. This prevents the regression from being reintroduced silently.
# checkov custom check skeleton
from checkov.common.models.enums import CheckResult
from checkov.github_actions.checks.base_github_action_check import BaseGithubActionsCheck
class QEMUSetupBeforeBuildx(BaseGithubActionsCheck):
def __init__(self):
super().__init__(
name="Ensure QEMU is registered before cross-platform buildx",
id="CKV_GHA_QEMU_001",
)
# Implement scan_resource_conf to parse job steps and enforce ordering