Home › DevOps › Docker Exec User Process Caused: Fix It Fast
Intermediate 5 min · September 23, 2026

Docker Exec User Process Caused: Fix It Fast

Match the image CPU to the host with --platform, fix the entrypoint shebang and CRLF endings, and ship multi-arch builds that run..

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 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 12 min
  • ✓Docker installed with buildx available on your machine
  • ✓A terminal where you can run docker and image inspect commands
  • ✓Basic familiarity with Dockerfiles, tags, and CPU architectures
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • This error means the kernel refused PID 1: usually an amd64 image on an arm64 host (or the reverse), deadly on M-series Macs
  • Prove it fast: compare uname -m against docker image inspect --format '{{.Architecture}}' for your image
  • Run now with docker run --platform linux/amd64, then rebuild correctly with docker build --platform or multi-arch buildx
  • If arches match, the entrypoint is broken: missing shebang, CRLF endings, or an interpreter your base image lacks
✦ Definition~90s read
What is Docker Exec Format Error Fix?

When Docker starts a container, the OCI runtime sets up namespaces, mounts, and cgroups, then asks the kernel to exec your entrypoint as PID 1. If the kernel can't run that file, the runtime reports failure with the prefix exec user process caused plus the kernel's reason in quotes.

★
Imagine hiring a translator who only speaks Spanish for a meeting held in Japanese.

The container never executes a single instruction of your code, so application debugging is pointless — the refusal happened one layer below your app.

The quoted cause splits the diagnosis. "exec format error" (ENOEXEC) means the file isn't runnable code for this CPU: an ELF binary built for amd64 on an arm64 host, or a text script with no #! shebang line naming an interpreter. Docker defaults docker build to the builder's native architecture, so M-series laptops silently emit arm64 images that amd64 servers reject, and amd64 CI emits images that fail on arm64 nodes.

Tags don't show architecture, tests rarely check it, and the mismatch travels invisibly to production.

"no such file or directory" is the sibling cause with the same prefix: the script names an interpreter that isn't in the image — #!/bin/bash on Alpine (which ships only /bin/sh), or any shell path in a scratch or distroless base that contains no shell at all. CRLF line endings produce the same flavor of failure by corrupting the interpreter name to /bin/sh plus a carriage return.

The cure depends on the quote: fix the architecture for ENOEXEC, fix the interpreter line and base image for missing files.

Plain-English First

Imagine hiring a translator who only speaks Spanish for a meeting held in Japanese. Everyone showed up, the room is booked, but no work can happen. That's this error: Docker built the room perfectly (namespaces, mounts, network), but the first process speaks the wrong CPU language — or hands the kernel a script with no translator named. You don't rebuild the room. You hire the right translator: the matching architecture or a fixed entrypoint.

The container is built, pushed, and pulled — and dies in under a second: standard_init_linux.go:228: exec user process caused "exec format error". Nothing ran. No app logs exist because the app never started. On Apple Silicon Macs shipping to Intel clusters (or Intel CI shipping to arm64 edge nodes), this line has ended more releases than any real bug.

The message has two halves. The prefix (exec user process caused) says PID 1 failed at the kernel's exec step, after the runtime assembled everything. The quoted cause names the refusal: exec format error means wrong CPU architecture or a broken script header, while no such file or directory means the named interpreter is missing from the image. Same prefix, different cure.

This guide orders the fixes by frequency: prove the arch mismatch, run now with --platform, rebuild with the right platform or multi-arch buildx, then fix the script family (shebang, CRLF, missing interpreter in slim bases). You'll know which half you're in within two commands.

Decode the Two Halves of the Error

The full line reads like riddles: standard_init_linux.go:228: exec user process caused "exec format error". (Newer runtimes phrase the prefix differently, but the shape is identical.) The prefix tells you where it died: the OCI runtime finished assembling the container — namespaces, mounts, cgroups all good — and the kernel refused to start your entrypoint as PID 1. That placement rules out image corruption (pulls would fail checksums), missing files at the runtime layer (that's an OCI create failure), and every application bug (no code ran).

The quoted cause tells you why it died, and it's the only part that picks the fix. "exec format error" is kernel ENOEXEC: the file isn't executable code for this CPU — wrong-architecture ELF binary, or a script with no valid #! interpreter line. "no such file or directory" means the script named an interpreter the image doesn't contain: /bin/bash on Alpine, any shell in scratch or distroless, or a shebang corrupted by CRLF into /bin/sh plus carriage return.

So the first move is always quoting the quote back at the incident thread: which exact string did we get? ENOEXEC sends you to architecture comparison and script headers; missing-file sends you to interpreter presence in the base image. Teams that skip this split chase both families at once and fix neither.

📊 Production Insight
Incident threads that paste the full quoted cause in the first message get to the fix twice as fast, because responders stop suggesting arch fixes for interpreter problems and vice versa.
🎯 Key Takeaway
Prefix says PID 1 died at exec; the quote picks the cure. Exec format error means arch or shebang, missing file means absent interpreter.

Prove the Arch Mismatch: Host vs Image

Diagnosis is a two-command comparison you can run in thirty seconds. uname -m prints the host CPU: x86_64 means amd64, aarch64 means arm64. docker image inspect --format '{{.Os}}/{{.Architecture}}' myapp:latest prints what the image was built for. Different answers are the entire bug — no logs to parse, no config to read. On Kubernetes, get the node side from kubectl get nodes with the architecture jsonpath and the deployed side from the pod's imageID digest, since tags move.

Beware the partial mismatches. A multi-arch manifest can list both platforms while a node pulls the wrong variant through a stale mirror. Worse, a right-arch image can smuggle a wrong-arch binary: a Go build with GOARCH unset, or a vendored amd64 sidecar copied into an arm64 image. When image and host agree but exec still fails, run file /app/server inside the image — it reports the ELF machine type per file and catches the binary that disagrees with its own manifest.

Write both values into the incident record before fixing. Arch bugs recur across laptop refreshes, CI migrations, and new node pools, and a written pair — host aarch64, image amd64 — turns the next occurrence into pattern matching. The file command on the exact failing path is the tiebreaker when manifest-level checks pass.

arch-proof.shBASH
1
2
3
4
5
6
uname -m
docker image inspect --format '{{.Os}}/{{.Architecture}}' myapp:latest
# Kubernetes: node arch vs what pods actually pulled
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name} {.status.nodeInfo.architecture}{"\n"}{end}'
# Suspect binary inside a right-arch image?
docker run --rm --entrypoint sh myapp:latest -c 'file /app/server'
📊 Production Insight
The file-per-binary check catches what manifests miss: an amd64 Go binary vendored into an arm64 image fails exec while every tag and manifest looks correct.
🎯 Key Takeaway
uname -m versus image Architecture decides the family. Use file on the failing binary when the manifest looks right.

Run It Now with --platform, Then Rebuild Right

When the container must run 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 and runs it — natively on amd64 hosts, emulated through QEMU or Rosetta on arm64 Docker Desktop. The flag also works on pull and compose (platform: per service), so the override slots into your normal workflow instead of demanding special tooling.

Know emulation's limits before you lean on it. QEMU user-mode emulation runs foreign code at a fraction of native speed — fine for a smoke test, miserable for a database — and Kubernetes nodes offer no emulation at all. So --platform is triage plus local-dev convenience, never the production strategy: it gets the container up now while you build the correct native image next for every machine.

Make the rebuild explicit too. docker build --platform linux/amd64 bakes the target CPU into the tag itself, so everyone who pulls gets the right bytes with no flags. That single flag in CI would have prevented both the laptop-push and the Graviton incidents: the platform becomes a reviewed build input instead of whatever machine happened to run the build that day.

platform-triage.shBASH
1
2
3
4
5
docker pull --platform linux/amd64 myapp:latest
docker run --rm --platform linux/amd64 myapp:latest --version
# Bake the arch into the tag so nobody needs the flag:
docker build --platform linux/amd64 -t myapp:latest .
docker image inspect --format '{{.Architecture}}' myapp:latest
📊 Production Insight
Emulation masks the bug on laptops while clusters burn: the wrong-arch image limps along under Desktop QEMU but dies instantly on kubelets with no emulation layer.
🎯 Key Takeaway
--platform runs the right arch now; --platform on build bakes it into the tag. Both are bridges to multi-arch, not the destination.

CRLF Entrypoints: The Invisible Carriage Return

When architectures match and exec still fails, check line endings before anything else. A start.sh edited or checked out on Windows carries CRLF (\r ) endings, which turns the shebang into #!/bin/sh plus a carriage return. The kernel looks for an interpreter literally named /bin/sh<CR>, finds nothing, and refuses — with an error that looks identical in every editor, because renderers hide the \r. This failure is architecture-independent, which perversely helps: identical failure on amd64 and arm64 means script family, no inspect needed.

Diagnose the bytes, not the text. head -1 file | od -c shows the truth: a healthy header ends , a corrupted one shows \r . Run that check inside the image (docker run with an sh entrypoint override), because the image layer is what the kernel reads — your laptop copy may already be fixed while the image still ships the poisoned bytes.

Fix it at three layers so it stays fixed. Convert the file with dos2unix or sed, add *.sh text=auto eol=lf to .gitattributes so Windows checkouts can't reintroduce CRLF, and add a CI smoke that boots the real entrypoint (not sh -c) so a regressed script fails the build. One line per layer, and this variant never returns.

crlf-triage.shBASH
1
2
3
4
5
6
7
docker run --rm --entrypoint sh myapp:latest -c 'head -1 /app/entrypoint.sh | od -c | head -3'
# Convert and lock LF endings in the repo:
sed -i 's/\r$//' entrypoint.sh
printf '*.sh text=auto eol=lf\n' >> .gitattributes
# Dockerfile keeps the exec bit regardless of checkout:
# COPY entrypoint.sh /app/entrypoint.sh
# RUN chmod +x /app/entrypoint.sh
📊 Production Insight
The od -c check takes ten seconds and ends all debate: if you see \r in the first line, stop the arch investigation entirely and fix endings.
🎯 Key Takeaway
CRLF corrupts the interpreter name invisibly. Check bytes with od, convert, and lock LF in .gitattributes.

Missing Shebang and Missing Interpreters in Slim Bases

Two more script-family triggers share one symptom. A missing shebang means the kernel finds a text file with no #! line and no interpreter to invoke — ENOEXEC, even though sh could run the file fine. The fix is mechanical: #!/bin/sh (or the real interpreter) as the file's first bytes. The lost exec bit is its twin: checkouts that strip +x produce a file the kernel won't exec, fixed with RUN chmod +x in the Dockerfile and verified with test -x inside the image.

The missing interpreter is subtler and base-image-specific. Alpine ships /bin/sh (BusyBox) but no /bin/bash, so any #!/bin/bash script dies there despite perfect arches and endings. Scratch and distroless ship no shell at all, so every shell entrypoint fails — the fix is exec-form CMD or ENTRYPOINT running the binary directly, with no shell in the chain. A dynamically linked binary in scratch fails the same way when its loader is absent; static builds (CGO_ENABLED=0 for Go) dodge that trap.

Match the script to the base deliberately. Use #!/bin/sh on Alpine, install bash only if you truly need it, and keep shell scripts out of scratch and distroless entirely. A CI smoke that boots the real entrypoint catches all three mistakes before they ship.

Dockerfile.fixed-entrypointDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
# Alpine: /bin/sh exists, /bin/bash does not — match the shebang.
FROM alpine:3.20
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# entrypoint.sh first line must be: #!/bin/sh
ENTRYPOINT ["/app/entrypoint.sh"]

# Scratch/distroless: no shell at all — exec the binary directly.
# FROM gcr.io/distroless/static AS prod
# COPY --from=build /app/server /app/server
# ENTRYPOINT ["/app/server"]
⚠ Never Assume /bin/bash Exists
Alpine has no bash, and scratch and distroless have no shell at all. Confirm the interpreter with ls inside the image, or skip shells entirely with exec-form ENTRYPOINT.
📊 Production Insight
Distroless migrations cause a predictable wave of these failures when shell-form ENTRYPOINT lines move over unchanged. Converting to exec form during the migration PR prevents the whole wave.
🎯 Key Takeaway
Shebang first line, exec bit in the image, interpreter present in the base. Verify all three inside the image, not on your laptop.

Ship Multi-Arch with buildx and Gate It in CI

The permanent fix is one tag holding native code for every CPU you run. Create a builder once with docker buildx create --use, 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, and exec format error becomes structurally impossible across those arches. No flags, no per-machine tags, no tribal knowledge.

Dockerfiles need modest discipline to build cleanly per arch. Base images must be multi-arch themselves (official images are), and RUN steps must not assume the builder's CPU — parameterize arch-specific downloads with TARGETARCH instead of hardcoding amd64 URLs. Compiled languages should build natively per platform leg rather than cross-compiling blindly; the classic failure is curl-ing an amd64 tarball that works on one leg and poisons the other.

Back the build with two gates. An architecture gate compares the pushed manifest platforms against your cluster's node arches and fails the pipeline on mismatch. An entrypoint gate boots the real entrypoint and runs the health check with no sh overrides. Restrict release-tag pushes to the CI identity so a laptop build can never overwrite release bytes. Verify with imagetools showing both platforms plus per-arch smokes before promotion.

buildx-multiarch.shBASH
1
2
3
4
5
docker buildx create --name multi --driver docker-container --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
docker buildx imagetools inspect myapp:latest | grep -E 'Platform|Digest'
docker run --rm --platform linux/amd64 myapp:latest --version
docker run --rm --platform linux/arm64 myapp:latest --version
📊 Production Insight
Shops that added the arch gate plus CI-owned tags report this error simply vanishing: the exact hand-push failure becomes unpushable within a week of the change.
🎯 Key Takeaway
buildx multi-arch plus an arch gate and CI-owned tags ends both the mismatch and the hand-push variants for good.
● Production incidentPOST-MORTEMseverity: high

CI-Built Amd64 Image Passed Tests, Died on 34 Arm64 Edge Nodes

Symptom
Monday's deploy rolled out and all 34 new edge nodes reported exec user process caused "exec format error" while the older amd64 nodes ran fine. The image tag was the same one that had passed CI twenty minutes earlier, the Dockerfile hadn't changed in weeks, and the rollout dashboard showed healthy pulls on every node. On-call assumed a corrupt registry mirror and spent half an hour re-pulling and re-pushing the tag.
Assumption
Because the tag was unchanged and CI was green, the team assumed the new nodes had a broken container runtime. They compared Docker versions between old and new nodes and found them identical, then suspected the node image. The actual change was the weekend hardware migration: the new nodes were arm64 Graviton, and the CI pipeline built amd64-only images nobody had ever needed to question.
Root cause
docker build in CI produced a single-arch amd64 image, and the scheduler placed the new pods on arm64 nodes whose kernels refused the amd64 ELF at exec. CI tested the image on amd64 runners, so green tests proved nothing about the new hardware. Architecture was never a build input, a test dimension, or a deploy gate.
Fix
They cordoned the arm64 nodes to stop the bleeding, then rebuilt with docker buildx for both linux/amd64 and linux/arm64 and pushed one multi-arch tag. An architecture gate comparing image manifest platforms against node architectures joined the pipeline the same day, so a single-arch push can never roll out again.
Key lesson
  • Node architecture is a deploy input, not background detail. Any hardware migration must trigger a rebuild-and-verify of every image the new nodes will run.
  • CI green on one arch proves nothing about another. Test and smoke the image on each CPU you schedule onto, or gate the rollout on manifest platforms.
  • Multi-arch tags make mixed fleets boring. One manifest with native code per CPU ends the laptop-versus-server and Intel-versus-Graviton wars permanently.
Production debug guideTwo commands decide the family; each fix below is a copy-paste sequence.5 entries
Symptom · 01
Container dies instantly and you don't know if it's arch or script
→
Fix
Run uname -m on the host and docker image inspect --format '{{.Os}}/{{.Architecture}}' myapp:latest. Different answers mean the arch family — use --platform and rebuild. Same answers mean the script family — check the shebang, endings, and interpreter next.
Symptom · 02
Architectures mismatch between image and host
→
Fix
Run now with docker pull --platform linux/amd64 myapp:latest && docker run --rm --platform linux/amd64 myapp:latest --version. Then rebuild for real with docker build --platform linux/amd64 -t myapp:latest . so the tag itself is correct and nobody needs the flag.
Symptom · 03
Arches match but the entrypoint script still fails
→
Fix
Read the bytes with docker run --rm --entrypoint sh myapp:latest -c 'head -1 /app/entrypoint.sh | od -c | head -3'. You want # ! /bin/sh with a clean newline. Missing #! or a \r means CRLF corruption: convert with dos2unix or sed -i 's/\r$//' and lock LF endings in .gitattributes.
Symptom · 04
Quoted cause is no such file or directory on a slim base
→
Fix
Confirm the interpreter exists with docker run --rm --entrypoint sh myapp:latest -c 'ls -l /bin/sh /bin/bash'. On Alpine there is no bash; on scratch and distroless there is no shell at all. Change the shebang to #!/bin/sh on Alpine, or switch to exec-form CMD running the binary directly.
Symptom · 05
One tag must run on both Intel and arm64 machines
→
Fix
Ship multi-arch: docker buildx create --use once, then docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push . Verify with docker buildx imagetools inspect myapp:latest showing both platforms, and smoke each with docker run --rm --platform on both arches.
Exec User Process Caused — Confirm, Fix, Prevent
Root CauseHow to ConfirmFixPrevention
Arm64 image on amd64 host (or reverse)uname -m disagrees with image Architecture; instant deathRun with --platform now; rebuild for the target CPUCI arch gate vs node arches; multi-arch buildx tags
CRLF-corrupted entrypoint scriptod -c shows \r\n in the shebang line inside the imageConvert to LF; lock eol=lf in .gitattributesCI smoke booting the real entrypoint
Missing shebang or lost exec bithead -1 shows no #!; test -x fails inside the imageAdd #!/bin/sh first line; RUN chmod +x in DockerfileLint script headers; verify perms in-image
Interpreter absent in slim basels shows no /bin/bash on Alpine or no shell in distrolessUse #!/bin/sh or exec-form binary ENTRYPOINTMatch scripts to base; smoke the real entrypoint in CI
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
arch-proof.shuname -mProve the Arch Mismatch
platform-triage.shdocker pull --platform linux/amd64 myapp:latestRun It Now with --platform, Then Rebuild Right
crlf-triage.shdocker run --rm --entrypoint sh myapp:latest -c 'head -1 /app/entrypoint.sh | od...CRLF Entrypoints
Dockerfile.fixed-entrypointFROM alpine:3.20Missing Shebang and Missing Interpreters in Slim Bases
buildx-multiarch.shdocker buildx create --name multi --driver docker-container --useShip Multi-Arch with buildx and Gate It in CI

Key takeaways

1
The prefix says PID 1 died at exec; the quoted cause picks arch versus interpreter fixes.
2
uname -m against image Architecture decides the family in two commands.
3
--platform on run is triage; --platform on build and multi-arch buildx are the cure.
4
CRLF corrupts the interpreter name invisibly
check bytes with od, lock LF endings.
5
Match scripts to the base
sh on Alpine, no shells in scratch or distroless.
6
Gate CI on arch-versus-nodes and real-entrypoint smokes; only CI pushes release tags.

Common mistakes to avoid

5 patterns
×

Treating it as a corrupt image and re-pulling

Symptom
Identical failure after every pull — the bytes are intact, built for the wrong CPU or wrapping a broken script.
Fix
Compare architectures first. Re-pull only with --platform for the right variant, or rebuild correctly.
×

Verifying the fix under emulation and declaring victory

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

Assuming the tested tag is the shipped tag

Symptom
Green CI, red production — tests ran against CI-built bytes while the tag was hand-overwritten from a laptop.
Fix
Compare digests between CI logs and running containers, and restrict release pushes to CI.
×

Checking script permissions on the laptop

Symptom
Laptop shows +x while the container fails — the image layer's mode is what the kernel sees.
Fix
Test with test -x inside the image and add RUN chmod +x to the Dockerfile.
×

Reading the entrypoint in an editor instead of od

Symptom
The shebang looks perfect everywhere while the kernel rejects it — the carriage return is invisible in renderers.
Fix
Inspect first bytes with od -c and enforce LF with .gitattributes.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does exec user process caused tell you about where the failure happ...
Q02JUNIOR
How do the two quoted causes change your fix?
Q03SENIOR
An image runs on your M-series Mac but dies in production. Why?
Q04SENIOR
Arches match but a shell entrypoint fails. What are your three checks?
Q05SENIOR
Design CI so a wrong-arch image can never reach the cluster.
Q01 of 05JUNIOR

What does exec user process caused tell you about where the failure happened?

ANSWER
PID 1 failed at the kernel's exec step after the runtime assembled the container. Namespaces and mounts worked; the entrypoint file itself was refused — so it's never an application bug.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why does it work on Docker Desktop but not on servers?
02
Is rebuilding without cache a fix?
03
How do I check a tag's architectures without pulling?
04
Why does file matter when inspect already shows the arch?
05
Does --platform slow my container down?
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 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Docker. Mark it forged?

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

←
Previous
Nginx Bind Address in Use Fix
25 / 25 · Docker
Next
Ansible SSH Connection Failed Fix
→