Fixing 'exec format error' on Apple Silicon
A container that starts fine on your teammate's Intel laptop dies instantly on an Apple Silicon Mac with exec /usr/local/bin/docker-entrypoint.sh: exec format error, because the image ships only a linux/amd64 binary and your machine runs an arm64 kernel. This page confirms the architecture mismatch, unblocks you immediately by forcing emulation, and then removes the problem permanently by building a multi-architecture image; it is part of cross-platform container builds within the broader containerized local environment patterns.
Diagnostic
The signature is a container that transitions straight to Exited (1) with an exec format error in the logs, not a crash inside your application code. The kernel is refusing to load the ELF binary at all because its machine type does not match the host CPU. Reproduce it and capture the exact message:
#!/usr/bin/env bash
set -euo pipefail
docker compose up api 2>&1 | head -n 20
docker compose ps --format 'table {{.Service}}\t{{.Status}}'
Expected BAD output — the process never reaches your entrypoint logic:
api-1 | exec /usr/local/bin/docker-entrypoint.sh: exec format error
api-1 exited with code 1
SERVICE STATUS
api Exited (1) 2 seconds ago
Confirm the root fact behind the message: the host is arm64 but the image was built for amd64. Compare the two directly. uname -m reports the host CPU; docker image inspect reports the architecture baked into the image manifest:
#!/usr/bin/env bash
set -euo pipefail
uname -m
docker image inspect --format '{{.Os}}/{{.Architecture}}' myorg/api:latest
On an Apple Silicon Mac this prints arm64 for the host and linux/amd64 for the image — a mismatch. To see whether the image even offers an arm64 variant, inspect its manifest list. A single-platform image lists only linux/amd64; a multi-arch image lists several rows:
#!/usr/bin/env bash
set -euo pipefail
docker buildx imagetools inspect myorg/api:latest \
| grep -E 'Platform|MediaType' | head -n 12
If the only Platform: line is linux/amd64, there is no native image for your Mac to pull, and the daemon fell back to the amd64 layer. That is the confirmed cause, and the resolution below applies directly.
One more check distinguishes a missing variant from a missing handler. If the container fails with exec format error rather than merely running slowly, no translation layer is catching the amd64 binary. Verify whether any emulation handler is registered at all before you change the Compose file:
#!/usr/bin/env bash
set -euo pipefail
docker run --rm --platform linux/amd64 alpine uname -m 2>&1 || true
If that command also prints exec format error instead of x86_64, the host has no working amd64 handler, and step one of the resolution installs one.
Root Cause
exec format error is the userspace name for the kernel error ENOEXEC: the execve syscall was handed a binary whose machine type the current CPU cannot decode. An x86-64 ELF header carries the machine value EM_X86_64; an Apple Silicon core is aarch64 and has no native path to run those instructions. When an image is published for a single platform, its manifest advertises exactly one os/arch pair. On an arm64 host with no matching variant and no translation layer registered, the daemon still unpacks the amd64 filesystem, but the first exec of any amd64 binary — the entrypoint, sh, your server — returns ENOEXEC and the container exits code 1.
The reason this surprises people is that it works everywhere else. On an Intel or AMD workstation the amd64 image runs natively, so the defect is invisible until an Apple Silicon machine joins the team. Two independent conditions must both hold for the failure: the image must lack an arm64 variant, and the host must lack a binfmt_misc handler (QEMU or Rosetta) that can translate amd64 instructions. Docker Desktop normally registers such a handler, which is why some amd64 images run — slowly — under emulation while others fail outright. When you see exec format error rather than a merely sluggous container, the emulation handler is missing, disabled, or was never installed for that architecture, so there is nothing to catch the ENOEXEC and translate it.
Resolution
Work in two passes: first force emulation so the existing amd64 image runs today, then build a multi-architecture image so the emulation crutch is no longer needed.
Install the emulation handlers so the kernel has a
binfmt_miscentry that can execute amd64 binaries onarm64. On Docker Desktop, enable Settings → General → "Use Rosetta for x86_64/amd64 emulation"; on Linux or CI, register the QEMU handlers explicitly:#!/usr/bin/env bash set -euo pipefail docker run --privileged --rm tonistiigi/binfmt --install all docker buildx lsPin the platform in Compose so the daemon deliberately requests the amd64 variant and runs it through the handler you just installed, instead of failing. Set it per service:
# docker-compose.yml services: api: image: myorg/api:latest platform: linux/amd64 ports: - "8080:8080"Bring it up with
docker compose up api. The container now starts because the emulated amd64 binary has a handler to run it. This is the unblock, not the cure — emulated startup and throughput are noticeably slower, so treat it as temporary.Create a multi-platform builder backed by the
docker-containerdriver, which is required to emit more than one architecture in a single build:#!/usr/bin/env bash set -euo pipefail docker buildx create --name xbuilder --driver docker-container --use docker buildx inspect --bootstrapBuild and push a multi-arch image so both
arm64andamd64clients pull a native layer. A manifest list build must push to a registry; the local daemon store cannot hold two architectures under one tag:#!/usr/bin/env bash set -euo pipefail docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag myorg/api:latest \ --push \ .Drop the
platform:override once the multi-arch tag exists, so each machine resolves its own native variant. Re-pull and confirm the image now advertises both architectures:#!/usr/bin/env bash set -euo pipefail docker compose pull api docker buildx imagetools inspect myorg/api:latest \ | grep -E 'Platform'
The two paths are not equivalent. Forcing platform: linux/amd64 keeps you on emulated x86 code — correct, but slower and heavier on battery and RAM. A multi-arch image lets the Apple Silicon machine run genuinely native arm64 code while Intel CI runners still pull amd64 from the same tag. Prefer the native build for anything you control; reserve emulation for third-party images whose Dockerfile you cannot change.
Expected Output
After the multi-arch build, the tag advertises two platforms and the container starts natively with no platform: override:
Platform: linux/amd64
Platform: linux/arm64
Bring the stack up and confirm the process is running native arm64 code rather than emulated x86. The architecture reported inside the container should now match the host:
#!/usr/bin/env bash
set -euo pipefail
docker compose up -d api
docker compose exec api uname -m
docker compose ps --format 'table {{.Service}}\t{{.Status}}'
Expected GOOD output — the entrypoint runs, uname -m reports aarch64, and the service stays up:
aarch64
SERVICE STATUS
api Up 6 seconds
The aarch64 line is the proof that matters: the container is executing native arm64 instructions, not amd64 under translation. There is no exec format error in the logs, startup is quick, and the same tag continues to serve amd64 to your Intel CI runners because the manifest list carries both variants under one name.
Prevention
The mismatch reappears the moment someone publishes a single-arch image or removes the platform matrix, so enforce architecture coverage rather than remembering it.
Set a default platform for shared third-party services so contributors on either CPU request the same variant deterministically. Export
DOCKER_DEFAULT_PLATFORM=linux/amd64in a checked-in.envfor images you cannot rebuild, and keep first-party images multi-arch so they resolve native without an override. When several Compose files layer environment values, confirm the variable survives the merge with env precedence resolution.Assert the manifest carries both architectures in CI so a single-arch regression fails the pipeline instead of a laptop. A one-line gate makes the invariant explicit:
#!/usr/bin/env bash set -euo pipefail archs=$(docker buildx imagetools inspect myorg/api:latest \ | grep -c 'linux/arm64' || true) test "$archs" -ge 1 || { echo "arm64 variant missing"; exit 1; }Always build first-party images through buildx with an explicit
--platformmatrix soamd64andarm64are produced together from one source and can never drift apart. Keeping local and CI builds on the same buildx invocation also protects the layer cache, which builds on the cache tactics in fast local rebuilds.
Platform caveats
Apple Silicon (ARM64): Rosetta 2 emulation is faster than QEMU but is x86-only — it translates
linux/amd64, not other architectures. Some amd64 images with hand-tuned SIMD ormmapassumptions still fault under Rosetta; when they do, a nativearm64build is the only reliable fix. macOS (Docker Desktop): The Rosetta toggle lives in Settings → General and requires a Docker Desktop restart to register thebinfmt_mischandler. Ifplatform: linux/amd64still yieldsexec format error, the handler did not install — restart the VM and re-check withdocker run --rm --platform linux/amd64 alpine uname -m. WSL2: The QEMU handlers registered bytonistiigi/binfmtdo not persist across a WSL restart. Re-run the--install allcommand afterwsl --shutdown, or add it to your shell profile so multi-arch builds keep working.
Rollback
#!/usr/bin/env bash
set -euo pipefail
git checkout HEAD -- docker-compose.yml
docker buildx rm xbuilder || true
docker compose pull api
docker compose up -d api
Frequently Asked Questions
What exactly does exec format error mean?
It is the userspace text for the kernel error ENOEXEC, returned when execve receives a binary whose machine type the CPU cannot decode. On an Apple Silicon Mac it almost always means the image contains linux/amd64 (x86-64) binaries and there is no emulation handler registered to translate them for the arm64 kernel, so the very first exec of the entrypoint fails and the container exits code 1.
Is setting platform: linux/amd64 a permanent fix?
No. It forces the daemon to request the amd64 variant and run it under emulation, which unblocks you but keeps the workload on translated x86 code — slower startup, higher memory use, and occasional faults in code that assumes native x86 behavior. Treat it as a temporary measure for third-party images. For any image you build yourself, publish a multi-arch manifest list so Apple Silicon pulls a native arm64 layer.
Why must a multi-arch buildx build use --push?
A multi-platform build produces a manifest list that references one image per architecture. The local Docker image store cannot hold two architectures under a single tag, so buildx writes the list directly to a registry with --push. If you only need one architecture locally, build a single --platform linux/arm64 image with --load instead, which the local daemon can store.
How do I check which architectures an image supports before pulling it?
Run docker buildx imagetools inspect <image> and read the Platform: lines. A single-arch image shows only one row, such as linux/amd64; a multi-arch image lists several, including linux/arm64. If linux/arm64 is absent you will need emulation on Apple Silicon, so decide up front whether to pin the platform or rebuild the image multi-arch.