The container starts, then dies with "exec format error." Here's why the CPU architecture is wrong, how to confirm it, and how to build images that just run.
You pull an image, run it, and instead of your app you get one useless line:
exec /usr/local/bin/app: exec format error
The container is technically fine. The layers are all there, the entrypoint exists, permissions are correct. The problem is that the binary inside was compiled for a CPU your host cannot run. Nine times out of ten this is an architecture mismatch: an arm64 image landing on an amd64 server, or the reverse.
This got a lot more common the day everyone switched to Apple Silicon. Your laptop is arm64. Your production servers are almost certainly amd64 (x86_64). You build locally, it runs beautifully on your machine, you push it, and the deploy box refuses to execute it. Same Dockerfile, same registry, completely different silicon.
When the kernel tries to run a binary, it reads the file header to figure out what instruction set it was built for. If that header says "this is ARM code" and the CPU speaks x86, the kernel bails out immediately with ENOEXEC, which surfaces as "exec format error." Docker just passes that message straight through.
So the error is not really a Docker error. It's the kernel telling you it was handed a program it doesn't know how to decode. Docker is the messenger.
Before you change anything, prove the mismatch. Three commands settle it.
Check what the image was built for:
docker image inspect myapp:latest --format '{{.Architecture}} / {{.Os}}'
# amd64 / linux or arm64 / linux
Check what your host actually is:
uname -m
# x86_64 -> you need amd64 images
# aarch64 -> you need arm64 images
And the platform Docker itself reports:
docker version --format '{{.Server.Arch}}'
If the image says arm64 and uname -m says x86_64, that's your whole problem right there. No further debugging needed.
For a multi-arch image already in a registry, you can see every variant it ships without pulling it:
docker buildx imagetools inspect myorg/myapp:latest
If your platform is missing from that list, the image was never built for you.
If a correct build exists in the registry, ask for it explicitly:
docker pull --platform linux/amd64 myorg/myapp:latest
docker run --platform linux/amd64 myorg/myapp:latest
This is the quick unblock, but treat it as a band-aid. If you have to hand --platform to every command, the real fix is building the image properly.
The right answer is to publish one image tag that carries both architectures, so nobody ever has to think about it again. Docker Buildx does this in a single command.
Set up a builder once:
docker buildx create --name multi --use --bootstrap
Then build for both and push in one shot:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myorg/myapp:latest \
--push .
That publishes a manifest list. When someone on an arm64 box pulls myapp:latest, they transparently get the arm64 layers; the amd64 server gets amd64. Same tag, correct binary, zero flags at runtime.
One catch worth knowing: --push is required for multi-platform builds. You can't --load a multi-arch image into your local Docker because a single local image can only hold one architecture. Build straight to the registry.
If your builder is amd64 but you need to produce arm64 layers (or vice versa), install the binfmt handlers so QEMU can emulate the foreign architecture during the build:
docker run --privileged --rm tonistiigi/binfmt --install all
After that, buildx can cross-build both platforms from one machine. It's slower because the foreign side runs under emulation, but it works. This is what most single-runner CI setups quietly rely on.
Sometimes the mismatch sneaks in through the base image. A FROM line without a digest can resolve to whatever the local daemon cached, which may not match what CI produces. If you're pinning, pin to a manifest that actually covers your targets, and let buildx pick the platform rather than hardcoding an arch-specific tag like arm64v8/node. Arch-specific base tags are the classic way to accidentally lock an image to one platform forever.
There's a second flavor of "exec format error" that has nothing to do with arm64 versus amd64. If your entrypoint is a shell script, the kernel reads the shebang on line one to find the interpreter. Get that wrong and you get the same error.
Usual causes:
#!/bin/sh, so the kernel has no interpreter to hand it to.#!/bin/sh\r makes the kernel look for an interpreter literally named sh\r, which doesn't exist. Run the script through dos2unix or set .gitattributes to keep shell scripts as LF./bin/bash in an image that only ships /bin/sh (looking at you, Alpine).Quick check inside the container:
head -1 /entrypoint.sh # is the shebang there and sane?
file /entrypoint.sh # "CRLF line terminators" is the smoking gun
Same error text, totally different root cause. The docker image inspect architecture check up top tells you instantly which of the two you're dealing with: if the arch matches your host, look at the script.
Stop building single-arch images by hand. The moment your team has even one Apple Silicon laptop and one x86 server, single-arch is a trap that will bite you on a Friday deploy. Move the buildx --platform linux/amd64,linux/arm64 --push build into CI so every published tag is multi-arch by default, and drop --platform from your run commands entirely. Runtime flags papering over a build problem are debt, and this is one of the cheapest debts to pay off. If you hit other variants of the same class of failure, our Docker troubleshooting guide maps the rest.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
The metrics stack you self-host is free software plus a real ops bill. Datadog hands you everything and mails you the invoice. Here's how we pick.
Static keys leak and live forever. Short-lived credentials from STS and Vault expire on their own β here's the token-exchange machinery and the TTL math that make it work.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.