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..
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓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
- 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
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.
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.
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.
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.
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.
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.
CI-Built Amd64 Image Passed Tests, Died on 34 Arm64 Edge Nodes
- 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.
| File | Command / Code | Purpose |
|---|---|---|
| arch-proof.sh | uname -m | Prove the Arch Mismatch |
| platform-triage.sh | docker pull --platform linux/amd64 myapp:latest | Run It Now with --platform, Then Rebuild Right |
| crlf-triage.sh | docker run --rm --entrypoint sh myapp:latest -c 'head -1 /app/entrypoint.sh | od... | CRLF Entrypoints |
| Dockerfile.fixed-entrypoint | FROM alpine:3.20 | Missing Shebang and Missing Interpreters in Slim Bases |
| buildx-multiarch.sh | docker buildx create --name multi --driver docker-container --use | Ship Multi-Arch with buildx and Gate It in CI |
Key takeaways
Common mistakes to avoid
5 patternsTreating it as a corrupt image and re-pulling
Verifying the fix under emulation and declaring victory
Assuming the tested tag is the shipped tag
Checking script permissions on the laptop
Reading the entrypoint in an editor instead of od
Interview Questions on This Topic
What does exec user process caused tell you about where the failure happened?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Docker. Mark it forged?
5 min read · try the examples if you haven't