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.

Where an amd64 binary fails to exec on an arm64 host The daemon pulls an amd64 layer, the arm64 kernel cannot load the ELF, and the container exits with exec format error. Why the exec fails Pull image amd64 layer only arm64 kernel reads ELF header ENOEXEC exec format error No arm64 handler means the kernel refuses to run the binary.
The daemon serves an amd64 binary, the arm64 kernel rejects its machine type, and the container exits before your entrypoint runs.

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.

  1. Install the emulation handlers so the kernel has a binfmt_misc entry that can execute amd64 binaries on arm64. 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 ls
  2. Pin 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.

  3. Create a multi-platform builder backed by the docker-container driver, 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 --bootstrap
  4. Build and push a multi-arch image so both arm64 and amd64 clients 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 \
      .
  5. 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'
Choosing emulation versus a native multi-arch build A decision based on whether you control the image build, leading to forced emulation or a multi-arch build. Emulate or Build Native? Do you build the image? own the Dockerfile No (third-party) pin platform: linux/amd64 Yes (own build) buildx multi-arch push
If you cannot rebuild the image, force emulation; if you own the build, publish a manifest list so every host pulls native.

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.

Forced emulation versus a native multi-arch image Comparison of running an amd64 image under emulation against publishing a multi-architecture manifest list. Emulation vs Multi-arch Force emulation platform: linux/amd64 runs today, no rebuild slower, higher RAM use: third-party image Multi-arch build buildx amd64 + arm64 native on every host one tag, full speed use: images you own
Emulation unblocks a third-party image immediately; a multi-arch build removes the mismatch for everyone at full native speed.

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/amd64 in a checked-in .env for 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 --platform matrix so amd64 and arm64 are 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.

Container start time by execution mode Bar chart comparing cold container start seconds for a failed amd64 exec, emulated amd64, and native arm64. Cold Start by Mode (seconds) amd64 (no handler) crash: exec error amd64 emulated 9.8s arm64 native 1.6s
The amd64 image cannot start without a handler; emulation runs but stays slow, while the native arm64 variant starts in a fraction of the time.

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 or mmap assumptions still fault under Rosetta; when they do, a native arm64 build is the only reliable fix. macOS (Docker Desktop): The Rosetta toggle lives in Settings → General and requires a Docker Desktop restart to register the binfmt_misc handler. If platform: linux/amd64 still yields exec format error, the handler did not install — restart the VM and re-check with docker run --rm --platform linux/amd64 alpine uname -m. WSL2: The QEMU handlers registered by tonistiigi/binfmt do not persist across a WSL restart. Re-run the --install all command after wsl --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.