Home DevOps Docker Exec Format Error: Fix Arch Mismatch
Intermediate 6 min · September 23, 2026

Docker Exec Format Error: Fix Arch Mismatch

Match the image architecture to the host with --platform, build multi-arch with buildx, or add the missing shebang.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 12 min
  • Docker installed (Desktop on Mac, Engine on Linux) with buildx available
  • A terminal where you can run docker and kubectl-style inspect commands
  • Basic familiarity with Dockerfiles, image tags, and CPU basics
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Exec format error means the kernel can't execute the binary — usually an amd64 image on an arm64 host (or the reverse), common on M-series Macs
  • Confirm both sides: uname -m for the host, docker image inspect --format '{{.Architecture}}' for the image — a mismatch is the diagnosis
  • Run cross-arch now with docker run --platform linux/amd64, and ship multi-arch images with docker buildx build --platform linux/amd64,linux/arm64
  • If architectures match, the entrypoint script itself is broken: missing #! shebang, lost exec bit, or Windows CRLF line endings
✦ Definition~90s read
What is Docker Exec Format Error Fix?

When Linux execs a file, the kernel reads its header to decide how to run it. ELF binaries name their machine type (x86-64 vs AArch64); scripts start with #! naming an interpreter. If the ELF machine type doesn't match the CPU, the kernel returns ENOEXEC — exec format error.

Think of a DVD from a different region — the disc is fine and the player is fine, but the player can't read that encoding and ejects it.

If a script has no shebang, the kernel likewise has no interpreter to invoke and returns the same error. Docker surfaces the kernel's refusal verbatim, which is why the message is terse: the kernel checked the first bytes, found code (or a script) it can't run here, and stopped before a single instruction executed.

The architecture path dominates today. Developers on arm64 Macs run docker build and get a native arm64 image by default; pushing that tag to an amd64 cluster (or pulling an amd64-only image onto the Mac) produces exec format error at container start.

Docker Desktop can emulate via Rosetta or QEMU, but Kubernetes nodes can't — the kubelet has no emulation layer, so wrong-arch images die instantly in production while running fine on the laptop. The script path is the runner-up: a start.sh without #!/bin/sh, without the exec bit, or with CRLF endings fails identically on every architecture.

What it is NOT: it isn't a corrupt download (corruption fails checksums at pull time), it isn't a missing file (that's OCI create failed territory), and it isn't an app bug (no code ran). Emulation flags and rebuilds are the arch cure; shebang, chmod +x, and LF endings are the script cure. One inspect command — image architecture vs host architecture — tells you which cure to apply.

Plain-English First

Think of a DVD from a different region — the disc is fine and the player is fine, but the player can't read that encoding and ejects it. Exec format error is the kernel ejecting your disc: the binary speaks amd64 while the chip speaks arm64 (or the reverse). M-series Macs made this daily news, as arm64 laptop builds meet amd64 servers. The fix is region-free discs (multi-arch images), the right disc (--platform), or fixing a scratched label (a broken shebang).

You built the image on your new MacBook, pushed it, and the cluster answered: "exec /app/server: exec format error." The binary is right there — you can see it in the image — yet the kernel won't touch it. Or the reverse: CI built it, your laptop won't run it. This error spikes every time a team mixes Apple Silicon laptops with Intel servers or Intel CI, and it always looks like a corrupt binary until you learn what the kernel is actually saying.

Exec format error (ENOEXEC) is the kernel reporting that a file isn't executable code for this machine: wrong CPU architecture, a script with no shebang line, or a shebang pointing at an interpreter that doesn't exist. The container assembled fine — mounts, namespaces, cgroups all worked — and then PID 1 failed at the first instruction. That's why it differs from OCI create failures: the runtime did its job, the binary couldn't do its.

This guide covers the two families in order: architecture mismatch (confirm, --platform, buildx multi-arch) and broken entrypoint scripts (shebang, exec bit, CRLF). You'll get the inspect commands that prove which family you're in and the build patterns that end it permanently.

What the Kernel Is Telling You

Exec format error is errno ENOEXEC, returned by the execve syscall when the kernel can't parse a file as runnable code for this CPU. Two triggers, no others: an ELF binary whose machine field (x86-64, AArch64) doesn't match the processor, or a text file without a valid #! interpreter line. The container runtime did everything right — namespaces, mounts, cgroups all assembled — and PID 1 died at the kernel boundary before executing anything. Your application code is provably innocent: it never ran.

This placement in the startup sequence is the diagnostic gift. OCI create failures happen before exec (assembly broke); immediate exits happen after exec (code ran and died). Exec format error is exactly at exec — so you check exactly two things: binary architecture vs host CPU, and script header vs interpreter. Nothing else in the stack can produce this errno, which makes the error one of Docker's most precise once you know the vocabulary.

The modern backdrop is Apple Silicon meeting Intel fleets. docker build defaults to the builder's native arch, so M-series laptops emit arm64 images that amd64 clusters reject, while CI on amd64 emits images that run emulated (slowly) or fail on arm64-only edge nodes. Tags don't record architecture visibly, tests don't check it, and dashboards don't show it — so the mismatch travels silently from laptop to registry to cluster, detonating only at exec.

🔥ENOEXEC Has Exactly Two Triggers
Wrong-architecture ELF binary, or a script with a missing, bad, or CRLF-corrupted shebang. If architectures match, it's the script — every time. This two-branch decision tree resolves the error in minutes.
📊 Production Insight
Teams new to mixed-arch fleets lose hours treating this as corruption or toolchain breakage. One inspect of image vs node architecture ends the debate — the Thursday incident burned 15 minutes on toolchain diffs first.
🎯 Key Takeaway
ENOEXEC at execve means wrong-arch binary or bad shebang — nothing else. Check arch vs CPU first, script header second.

Confirm the Mismatch: Host vs Image Architecture

Diagnosis is a two-command comparison. uname -m prints the host CPU: x86_64 (amd64) or aarch64 (arm64). docker image inspect --format '{{.Os}}/{{.Architecture}}' prints what the image was built for. Different answers is the whole diagnosis — no logs to read, no config to parse. On Kubernetes, get the node side with kubectl get nodes -o jsonpath='{...status.nodeInfo.architecture}' and the deployed side from the pod's imageID digest, since the tag may have moved since the failing pull.

Watch for the partial cases. An image can be multi-arch at the manifest level while the node pulled the wrong variant — rare, but check with buildx imagetools inspect. A binary inside a right-arch image can still be wrong-arch: a Go binary cross-compiled with GOARCH unset in a Dockerfile that assumed the builder's arch, or a vendored amd64 sidecar copied into an arm64 image. When image and host agree but exec still fails, exec into the image and run file /app/server — it reports the ELF machine type per file, catching the binary that disagrees with its own image.

Record both values in the incident thread before fixing. Architecture bugs recur across teams (laptop fleets, CI migrations, new node pools), and a written pair of values — host aarch64, image amd64 — makes the next occurrence a pattern match instead of a fresh mystery. The file command on the exact failing path is the tiebreaker when image-level checks pass.

arch-mismatch-confirm.shBASH
1
2
3
4
5
6
7
8
9
10
11
# The two-command diagnosis: host CPU vs image arch
uname -m
docker image inspect --format '{{.Os}}/{{.Architecture}}' myapp:latest

# Kubernetes: node arch vs what the pod actually pulled
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name} {.status.nodeInfo.architecture}{"\n"}{end}'
kubectl get pod -l app=myapp -o jsonpath='{..imageID}' | tr ' ' '\n' | sort -u

# Right image, wrong binary inside? Check the exact failing path
docker run --rm --entrypoint sh myapp:latest -c 'file /app/server || head -c 4 /app/server | od -An -tx1'
# Want: ELF 7f 45 4c 46 + matching machine; scripts must start with #!
📊 Production Insight
The file command catches what image-level checks miss: an amd64 Go binary vendored into an arm64 image fails exec while every manifest looks correct. Always check the exact failing path.
🎯 Key Takeaway
uname -m vs image Architecture decides the family. file on the failing binary catches arch bugs hiding inside right-arch images.

Run It Now: the --platform Flag

When you need the container running in the next five minutes, --platform overrides architecture selection at pull and run time. docker run --platform linux/amd64 pulls the amd64 variant of a multi-arch tag (or fails loudly on single-arch tags built for the other CPU) and runs it — natively on amd64 hosts, emulated on arm64 via Docker Desktop's Rosetta or QEMU. The same flag works on pull, build, and compose (platform: in the service), so the override follows your normal workflow instead of requiring special commands.

Understand emulation's limits before leaning on it. QEMU user-mode emulation runs amd64 code on arm64 at a fraction of native speed — fine for a smoke test, painful for a database, and unavailable on Kubernetes nodes entirely. Rosetta on Docker Desktop is faster but Desktop-only. So --platform is triage and local development convenience, not a production strategy: it gets the container up now while you build the correct native image next.

Use --platform deliberately in heterogeneous workflows even without errors. CI that must test the production arch from arm64 runners, developers verifying the amd64 artifact on M-series laptops, and migration periods with mixed node pools all benefit from explicit flags. Explicit beats ambient: a flag in the command documents the intent, while silent arch selection is how wrong-arch tags get pushed in the first place.

platform-override-run.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Pull and run the production arch explicitly (triage + verification)
docker pull --platform linux/amd64 myapp:latest
docker run --rm --platform linux/amd64 myapp:latest --version

# Compose equivalent for local dev against the prod arch
# services:
#   app:
#     image: myapp:latest
#     platform: linux/amd64

# Check what a tag actually offers before overriding blindly
docker buildx imagetools inspect myapp:latest | grep -E 'Name|Platform'

# Emulation check on Desktop: expect slower startup, full function
docker run --rm --platform linux/amd64 myapp:latest sh -c 'uname -m && echo EMULATED-OK'
📊 Production Insight
Emulation masks the bug on laptops while production burns: the wrong-arch image runs (slowly) under Desktop QEMU but dies instantly on kubelets with no emulation layer. Verify native, not emulated.
🎯 Key Takeaway
--platform gets you running now and documents arch intent. It's triage and dev convenience — production needs native images.

Ship Multi-Arch: buildx Once, Run Anywhere

The permanent fix is one tag containing native code for every CPU you run: a multi-arch manifest list built with docker buildx. Create a builder once (docker buildx create --use, with the container driver for multi-platform), then build with --platform linux/amd64,linux/arm64 and push. The registry stores one tag pointing at two images; each machine pulls its native variant automatically — M-series laptops get arm64, Intel clusters get amd64, and nobody passes flags. Exec format error becomes structurally impossible across those arches.

Dockerfiles need modest discipline to build cleanly per arch. Base images must themselves be multi-arch (official images are), RUN steps must not assume the builder's arch (use BUILDARCH-aware conditionals or TARGETARCH args when downloading arch-specific binaries), and compiled languages should build natively per platform rather than cross-compiling blindly. The classic failure is curl-ing an amd64 tarball in a RUN step: it works on the amd64 leg and dies on arm64. Parameterize with $TARGETARCH and both legs stay green.

Verify the artifact, not the intent. buildx imagetools inspect must show both platforms, and a smoke run per arch (native runners or --platform pulls) must pass before the tag ships. In CI, build both legs on every release and gate promotion on the dual smoke. Teams that adopt this stop thinking about architecture entirely — the manifest absorbs the laptop-vs-cluster difference that used to page people.

buildx-multiarch-ship.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# One-time: a builder that can do multi-platform
docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap

# Build both arches, push one tag
# (Dockerfile must honor $TARGETARCH for arch-specific downloads)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .

# Verify the manifest really holds both
docker buildx imagetools inspect myapp:latest | grep -E 'Platform|Digest'

# Smoke each arch natively (or via --platform on mixed runners)
docker run --rm --platform linux/amd64 myapp:latest --version
docker run --rm --platform linux/arm64 myapp:latest --version
📊 Production Insight
The $TARGETARCH trap: hardcoding an amd64 download URL in RUN breaks only the arm64 leg, and CI that builds one arch never sees it. Parameterize every arch-specific fetch.
🎯 Key Takeaway
buildx with both platforms plus a pushed manifest ends the war. Honor TARGETARCH in Dockerfiles and smoke both arches in CI.

Shebang, Exec Bit, CRLF: the Script Family

When architectures match and exec still fails, the entrypoint script is broken in one of three ways. Missing shebang: the kernel has no interpreter to invoke, so ENOEXEC even though sh could run the file fine — add #!/bin/sh (or the real interpreter) as byte one. Lost exec bit: COPY from some contexts (notably Windows checkouts) strips +x, so chmod +x in the Dockerfile with an explicit RUN chmod. CRLF line endings: the shebang becomes #!/bin/sh<CR>, naming an interpreter with a carriage return that doesn't exist — invisible in editors, fatal to the kernel.

Diagnose the bytes, not the text. head -1 file | od -c shows the truth: # ! /bin/sh is healthy, missing #! is obvious, and \r is the CRLF smoking gun. test -x confirms the exec bit inside the image (not on your laptop — the image is what runs). And verify the interpreter ships in the base: Alpine has /bin/sh but no /bin/bash, slim images drop shells you assumed, distroless has no shell at all — a #!/bin/bash shebang in those images fails exactly like a missing file.

Fix at every layer. Dockerfile: COPY then RUN chmod +x, with the shebang as the file's first line. Repo: .gitattributes forcing text=auto eol=lf on *.sh so Windows checkouts can't reintroduce CRLF. CI: a smoke that boots the real entrypoint (not sh -c) so script breakage fails builds. The script family is entirely preventable — each guard is one line, and together they close it forever.

entrypoint-script-triage.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Read the bytes, not the text: want '# ! /bin/sh \n', no \r
docker run --rm --entrypoint sh myapp:latest -c 'head -1 /app/start.sh | od -c | head -3'

# Exec bit inside the image (laptop perms don't matter)
docker run --rm --entrypoint sh myapp:latest -c 'test -x /app/start.sh && echo EXEC-OK || echo MISSING-X-BIT'

# Interpreter must exist in the base image
docker run --rm --entrypoint sh myapp:latest -c 'ls -l /bin/sh /bin/bash 2>&1'

# Fixes (Dockerfile + repo):
# COPY start.sh /app/start.sh
# RUN chmod +x /app/start.sh        # restore exec bit
# first line of start.sh: #!/bin/sh # real interpreter only
# .gitattributes: *.sh text=auto eol=lf  # CRLF can never return
⚠ CRLF Is Invisible and Fatal
Windows checkouts turn #!/bin/sh into #!/bin/sh + carriage return, and the kernel reports the whole line as missing. If exec fails on a script that looks perfect, check od -c for \r before anything else — and lock .gitattributes so it stays fixed.
📊 Production Insight
CRLF entrypoints fail on every architecture identically, which perversely helps: arch-independent failure means script family, no inspect needed. The od -c check takes ten seconds.
🎯 Key Takeaway
Shebang first line, exec bit in image, LF endings, interpreter present in base. Guard with chmod, .gitattributes, and a real-entrypoint smoke.

Lock It In: Gates That End This Error

Two gates close both families permanently. The architecture gate runs in CI after the push: inspect the pushed image's .Architecture (and manifest platforms for multi-arch) and compare against the architectures of your clusters and laptop fleet. Mismatch fails the pipeline before any rollout starts — the Thursday hand-push would have died right there with a message naming arm64 vs amd64. The entrypoint gate boots the image with its real entrypoint and runs the health check: no sh overrides, no --platform crutches, the exact command production will exec.

Back these with access hygiene. Humans shouldn't push release tags — restrict registry writes to the CI identity so laptop builds can't overwrite release bytes. Pin base images by digest so upstream arch changes arrive as deliberate PRs, not surprises. And document the fleet's architectures in one place: which clusters are amd64, which edge nodes are arm64, what developers run — so build platform lists stay correct as hardware changes.

Verify the whole chain after adopting multi-arch: imagetools shows both platforms, per-arch smokes pass, the arch gate is green, and a canary pod on each node type starts clean. Exec format error is a build-time fact meeting a run-time CPU; checking the fact before the meeting means they never disagree again. That's the entire strategy: prove arch and entrypoint in CI, and production only ever execs what the kernel can run.

exec-gates-verify.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
# Gate 1: pushed arch must match the cluster (fail pipeline otherwise)
IMG_ARCH=$(docker image inspect --format '{{.Architecture}}' myapp:${TAG})
CLUSTER_ARCHES=$(kubectl get nodes -o jsonpath='{range .items[*]}{.status.nodeInfo.architecture}{"\n"}{end}' | sort -u)
echo "image=$IMG_ARCH cluster: $CLUSTER_ARCHES"
echo "$CLUSTER_ARCHES" | grep -qx "$IMG_ARCH" || { echo 'ARCH MISMATCH — blocking rollout'; exit 1; }

# Gate 2: boot the REAL entrypoint, no overrides
# docker run --rm myapp:${TAG} /healthcheck

# Post-adoption proof: manifest + per-arch smokes + canary
# docker buildx imagetools inspect myapp:${TAG} | grep Platform
kubectl rollout status deploy/myapp --timeout=120s
kubectl logs -l app=myapp --tail=3
📊 Production Insight
Registry write restrictions for humans plus an arch gate in CI ended hand-pushed wrong-arch tags for good at one org — the exact Thursday failure became unpushable within a week.
🎯 Key Takeaway
Gate on image-vs-cluster arch, smoke the real entrypoint, restrict tag pushes to CI. Verify manifest, smokes, and canary after the switch.
● Production incidentPOST-MORTEMseverity: high

Arm64 Laptop Builds Died on 19 Amd64 Pods for 26 Minutes

Symptom
At 3:40 PM on a Thursday, a routine release rolled out and all 19 new pods entered CrashLoopBackOff within a minute, each logging "exec /app/server: exec format error" and dying in under a second. The deploy had passed CI — tests green, image pushed, rollout started normally. The previous 40 releases from the same pipeline had been clean. On-call assumed a Go toolchain upgrade had emitted a bad binary and started rolling back the compiler version.
Assumption
Because the Dockerfile and CI config hadn't changed, the team assumed the base image or toolchain had silently switched architectures. Two engineers spent 15 minutes diffing go.mod toolchains and base image digests. The actual change was human, not technical: the release engineer rebuilt the image locally on a new M2 MacBook to "save CI minutes" and pushed the tag by hand, overwriting the CI-built amd64 image with an arm64 one. CI tests had run against the earlier amd64 image, so green tests proved nothing about the pushed bytes.
Root cause
docker build on the M2 Mac produced a native linux/arm64 image, and the manual push overwrote the release tag. The production cluster is amd64-only with no emulation, so the kernel on every node refused the arm64 ELF at exec. Image architecture (arm64) vs node architecture (amd64) was the entire bug — invisible in the tag, the tests, and the rollout dashboard.
Fix
Immediate: retagged the previous amd64 image and rolled back — pods healthy 26 minutes after the first failure. Same day: locked the release tag to CI-built images only (registry write perms removed from humans) and added a pipeline gate comparing image architecture to the cluster's node architectures. That month: switched the build to docker buildx multi-arch (amd64 plus arm64) so laptop and cluster each pull native code.
Key lesson
  • Never hand-push release tags from a laptop. CI-built, arch-checked images only — human pushes bypass every gate and the tag hides the architecture switch.
  • Gate deploys on image architecture, not just tests. Tests ran against different bytes than production; an inspect of .Architecture against node arches would have failed the push in seconds.
  • Multi-arch images end the laptop-vs-cluster war permanently. One manifest, native code per node — M-series laptops and Intel clusters stop fighting.
Production debug guideFive symptoms, five exact command sequences — compare architectures first, scripts second.5 entries
Symptom · 01
Container dies instantly with exec format error and you don't know which family it is
Fix
Compare the two architectures: run uname -m on the host (x86_64 vs aarch64) and docker image inspect --format '{{.Os}}/{{.Architecture}}' myapp:latest for the image. Mismatch means the arch family — skip to --platform and buildx. Match means the script family — inspect the entrypoint's shebang, exec bit, and line endings next.
Symptom · 02
Architectures mismatch — arm64 image on amd64 (or the reverse)
Fix
Run it now with an explicit platform: docker pull --platform linux/amd64 myapp:latest && docker run --rm --platform linux/amd64 myapp:latest. On Docker Desktop, enable Rosetta/ QEMU emulation for ad-hoc runs. For Kubernetes, there is no emulation — you must ship the right arch, so move to the buildx fix and don't waste time on node flags.
Symptom · 03
You need one tag that runs on both laptops and cluster nodes
Fix
Build multi-arch with buildx: docker buildx create --use (once), then docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push . Verify both arches landed with docker buildx imagetools inspect myapp:latest showing two manifests. Repull per machine and confirm native execution — no --platform flag needed anymore.
Symptom · 04
Architectures match but a script entrypoint still fails
Fix
Read the script's first bytes: docker run --rm --entrypoint sh myapp:latest -c 'head -1 /app/start.sh | od -c | head -3' — you want # ! /bin/sh with \n endings, not \r \n. Check the exec bit with test -x, and confirm the interpreter exists in the image (ls -l /bin/sh). Fix with a proper shebang, chmod +x in the Dockerfile, and LF endings via .gitattributes.
Symptom · 05
CI is green but production dies with exec format error
Fix
Prove CI tested the shipped bytes: compare the digest CI tested against the deployed digest (kubectl get pod -o jsonpath for imageID vs CI logs). Add a gate that inspects architecture before rollout: docker image inspect --format '{{.Architecture}}' must equal the cluster arch (kubectl get nodes -o jsonpath for kubelet arches). Fail the pipeline on mismatch so a hand-pushed wrong-arch tag can never roll out.
Exec Format Error — Confirm, Fix, Prevent
Root CauseHow to ConfirmFixPrevention
Arm64 image on amd64 host (or reverse)uname -m disagrees with image .Architecture; instant death on kubeletsRun now with --platform; ship the native archCI arch gate vs cluster arches; multi-arch buildx images
Single-arch tag pushed from laptopimagetools shows one platform; digest differs from CI-built oneRetag CI-built image and roll back; rebuild multi-archHumans can't push release tags; CI owns release bytes
Missing or wrong shebanghead -1 shows no #! or names absent interpreter like /bin/bash on AlpineAdd #!/bin/sh first line; match interpreter to the base imageSmoke the real entrypoint in CI; lint script headers
Lost exec bit or CRLF endingstest -x fails in image; od -c shows \r\n line endingsRUN chmod +x in Dockerfile; convert to LF endings.gitattributes forces LF on *.sh; checkout checks in CI
Wrong-arch binary inside right-arch imagefile /app/server reports mismatched ELF machine typeFix GOARCH/TARGETARCH in build; vendor correct binaryParameterize arch-specific downloads with $TARGETARCH
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
arch-mismatch-confirm.shuname -mConfirm the Mismatch
platform-override-run.shdocker pull --platform linux/amd64 myapp:latestRun It Now
buildx-multiarch-ship.shdocker buildx create --name multi --driver docker-container --useShip Multi-Arch
entrypoint-script-triage.shdocker run --rm --entrypoint sh myapp:latest -c 'head -1 /app/start.sh | od -c |...Shebang, Exec Bit, CRLF
exec-gates-verify.shIMG_ARCH=$(docker image inspect --format '{{.Architecture}}' myapp:${TAG})Lock It In

Key takeaways

1
ENOEXEC means wrong-arch ELF or bad shebang
the kernel refused before anything ran.
2
uname -m vs image Architecture decides the family in two commands.
3
--platform runs the right arch now; emulation is triage, never production strategy.
4
buildx multi-arch (amd64 plus arm64) makes the error structurally impossible.
5
Scripts need #!, exec bit in the image, LF endings, and an interpreter the base ships.
6
Gate CI on arch-vs-cluster and real-entrypoint smokes; only CI pushes release tags.

Common mistakes to avoid

6 patterns
×

Treating it as a corrupt image and re-pulling

Symptom
Same error after every pull — the bytes are intact, they're just built for a different CPU. Re-pulling correct bytes of the wrong arch changes nothing.
Fix
Compare architectures first. Re-pull only with --platform for the right variant, or rebuild for the target CPU.
×

Testing the fix under emulation and declaring victory

Symptom
Runs on the MacBook via QEMU, dies on every kubelet — emulation masked the arch gap that production can't bridge.
Fix
Verify native execution per target. Emulation is triage; native images per arch are the fix.
×

Assuming the tag you tested is the tag you shipped

Symptom
Green CI, red production — tests ran against CI-built amd64 bytes while the deployed tag was hand-overwritten with arm64.
Fix
Compare digests between CI logs and the running pods. Lock tag pushes to the CI identity.
×

Checking script permissions on the laptop

Symptom
Laptop shows +x, container still fails — the exec bit in the image layer is what the kernel sees, and COPY may not have preserved it.
Fix
Test with test -x inside the image and add RUN chmod +x in the Dockerfile.
×

Reading the script in an editor instead of od

Symptom
Shebang looks perfect in every editor while the kernel rejects it — the \r is invisible in normal rendering.
Fix
Inspect first bytes with od -c. Enforce LF with .gitattributes so the fix sticks across Windows checkouts.
×

Hardcoding amd64 downloads in a multi-arch Dockerfile

Symptom
amd64 leg builds, arm64 leg fails (or ships a foreign binary that dies at exec) — one arch never gets tested.
Fix
Parameterize every arch-specific fetch with $TARGETARCH and smoke both legs in CI.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does exec format error mean at the kernel level?
Q02JUNIOR
An image runs on your M-series Mac but dies with exec format error in pr...
Q03SENIOR
Architectures match but a shell entrypoint still fails. List your three ...
Q04SENIOR
How do --platform and buildx multi-arch differ, and when is each right?
Q05SENIOR
Design CI so a wrong-arch image can never reach the cluster.
Q01 of 05JUNIOR

What does exec format error mean at the kernel level?

ANSWER
The execve syscall returned ENOEXEC: the file isn't runnable code for this CPU — either an ELF binary with a non-matching machine type, or a script without a valid #! interpreter line. Nothing executed, so it's never an app bug.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why does the image work on Docker Desktop but not Kubernetes?
02
Can I mix amd64 and arm64 nodes in one cluster?
03
Does rebuilding without cache fix exec format error?
04
How do I check a registry tag's architectures without pulling?
05
Why does file matter when inspect already shows the arch?
06
Are distroless images more prone to this?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Docker. Mark it forged?

6 min read · try the examples if you haven't

Previous
Docker No Space Left on Device Fix
24 / 24 · Docker
Next
Kubernetes Unbound PVC Fix