Fix Docker "exec format error" (Architecture Mismatch)
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.
Key takeaways
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.
On this page
Fix Docker "exec format error" (Architecture Mismatch)#
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.
What "exec format error" actually means#
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.
Diagnosis: confirm it's the architecture#
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.
Fix 1: pull and run the right platform#
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.
Fix 2: build multi-arch with buildx (the real fix)#
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.
Fix 3: QEMU emulation when you must build the other arch locally#
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.
Fix 4: pin your base image to the right arch#
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.
The variant that isn't about CPU: scripts#
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:
- No shebang at all. The file starts straight into commands with no
#!/bin/sh, so the kernel has no interpreter to hand it to. - Windows line endings. A
#!/bin/sh\rmakes the kernel look for an interpreter literally namedsh\r, which doesn't exist. Run the script throughdos2unixor set.gitattributesto keep shell scripts as LF. - A shebang pointing at
/bin/bashin 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.
The call we'd make#
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 DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon β CrashLoopBackOff, OOMKilled, Terraform state locks, and more β plus new guides as we publish them.
Kubernetes Workload Identity β Projected Tokens and OIDC to Cloud IAM
How pods can talk to AWS, GCP, and Azure with no static keys β using audience-bound projected ServiceAccount tokens and the cluster OIDC issuer.
GitLab CI/CD Best Practices in 2026: Pipelines You Can Trust
A production-focused GitLab CI/CD guide: include/extends templates, rules and workflow, needs DAGs, cache vs artifacts, protected environments, masked/protected variables and Vault, built-in security scanning, and review apps β with copy-paste examples.
More from DevOps
Explore more articles in this category
Best Kubernetes IDE and GUI Tools in 2026
kubectl is fine until you're juggling five namespaces across three clusters. These are the tools that make that manageable, compared.
Chef vs Puppet vs Ansible: Configuration Management in 2026
One is agentless and Python-based, the other two run a persistent agent and a domain-specific language. The architecture difference matters more than the syntax.
PagerDuty vs Opsgenie: Choosing an Incident Alerting Tool
Both page the right person at 3am and both integrate with everything. The real differences show up in pricing structure, workflow depth, and who already owns the ecosystem around you.
You might have missed
Evergreen posts worth revisiting.