Executable File Not Found: 4 Reasons Docker Containers Exit Instantly (incl. exec format error)
Symptom: the container dies a second after starting
# At container start
standard_init_linux.go: exec: "./start.sh": stat ./start.sh: no such file or directory
# Or with docker exec
OCI runtime exec failed: exec: "sh": executable file not found in $PATH: unknown
# Or another form
exec ./start.sh: no such file or directory
One-line diagnosis: the file or command you told the container to execute can't be found inside it. The trap: when it says no such file or directory, the file is often right there — what's actually missing is the script's interpreter (the program the shebang points to). This is the most misleading error message Docker has.
Before rewriting the Dockerfile, copy the full error text (standard_init_linux.go, OCI runtime, or exec format error) and match it against the four causes below. Most tickets end in cause one (CRLF) or cause four (architecture).
Four causes, ranked by frequency
1. Windows CRLF line endings broke the shebang (the sneaky one)
A shell script edited on Windows has \r\n endings, so line one actually reads:
#!/bin/sh\r
Linux looks for a program called sh\r — it doesn't exist, so you get no such file or directory while ls shows the script sitting right there. Verify and fix:
docker run --rm --entrypoint sh myapp -c "head -1 /app/start.sh | od -c | head -2"
# \r \n in the output is the smoking gun
# Fix: add to the repo's .gitattributes
*.sh text eol=lf
Or switch the editor from CRLF to LF and re-save. If a Windows CI runner checks out the scripts, pin eol=lf or you will "fix locally" and break again in the pipeline.
2. ENTRYPOINT exec form with variable interpolation
# ❌ exec form never spawns a shell: $JAVA_OPTS stays literal
ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", "app.jar"]
# ✅ to expand variables use shell form (note: loses signal forwarding)
ENTRYPOINT java ${JAVA_OPTS} -jar app.jar
Exec form ["a","b"] starts no shell — $VAR, ~, and globs stay literal; shell form a b goes through /bin/sh -c, so variables expand. Confusing the two forms is a top source of this error. Likewise, ENTRYPOINT ["npm", "start"] failing to find npm usually means npm isn't on PATH in the base image or lives under another user's home.
A practical compromise: a tiny entrypoint script that ends with exec of the real process — you keep expansion and keep signals. Scaffold with the Dockerfile generator, then hand-edit the entrypoint.
3. Paths: relative paths and WORKDIR
WORKDIR /app
COPY start.sh . # file is at /app/start.sh
ENTRYPOINT ["./start.sh"] # ✅ relative to WORKDIR
ENTRYPOINT ["/start.sh"] # ❌ nothing at root
Fastest check is to exec in and look around:
docker run --rm --entrypoint sh myapp -c "ls -la /app && which npm"
In multi-stage builds, a missing COPY --from=builder path or a lost executable bit looks identical from the outside: "it was in the build context." Confirm with ls -la inside the final image; put chmod +x in the Dockerfile instead of fixing permissions only on your laptop.
4. Architecture mismatch: exec format error
exec /app/start.sh: exec format error
An arm64 image built on an M-series Mac running on an x86 server (or vice versa). Not a missing file — the binary format is wrong. Lock the platform at build time:
docker buildx build --platform linux/amd64 -t myapp --push .
Confirm with docker inspect --format '{{.Architecture}}' myapp.
The debugging trio
# 1. What entry did the image actually configure?
docker inspect myapp --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'
# 2. Bypass the entrypoint and inspect the scene
docker run --rm --entrypoint sh myapp
# inside: ls the files, head -1 xxx.sh | od -c for CRLF, which xxx for PATH
# 3. Run pieces of the Dockerfile step by step — the failing step reveals itself
docker run --rm myapp ls /app
Minimal bases (scratch, distroless) often ship without sh — so --entrypoint sh itself fails with not-found. Switch to a debug-tagged image that includes a shell, or docker create + docker cp the binary out and run host file on it.
ENTRYPOINT vs CMD: how the shell gets lost
The error's most common root is a shell idiom difference. CMD npm start (shell form) runs /bin/sh -c "npm start" — PATH lookup works, but no signals reach npm (PID 1 is sh). CMD ["npm","start"] (exec form) skips the shell entirely — fine for absolute paths, broken when the binary needs PATH or shell expansion. Windows-path footgun: a Dockerfile edited on Windows gets a trailing \r — the container looks for a binary literally named npm\r. The robust pattern: exec form with absolute paths, and .gitattributes marking Dockerfile as LF.
When migrating historical docker run lines into Compose, the same array-versus-string distinction applies to entrypoint / command. Use docker run to Compose as a starting point, then verify you did not accidentally rely on shell expansion.
The architecture silent-failure
A binary built for the wrong architecture does not always fail with "exec format error" — sometimes it manifests as file-not-found when the loader is missing. file /app/binary inside the container tells the truth in one line: x86-64 vs aarch64. The cross-build case sneaks in via CI runners (arm-native) building for default hosts, and via Rust/Go binaries copied from a Mac into a linux/amd64 image. Lock GOARCH / --target at compile time; "build on the laptop, COPY into the image" is not a cross-platform strategy.
ENTRYPOINT scripts: exec or orphan
A custom entrypoint.sh that ends with node server.js (without exec) makes sh the PID 1 — signals stop, docker stop escalates to SIGKILL after timeout, and graceful shutdown is gone. The last line must be exec "$@". This does not cause file-not-found directly, but the debugging detour usually passes through both symptoms together.
When the image has no bash (and no npm)
If the message is exec: "bash": executable file not found, inspect the base: Alpine ships BusyBox sh, not bash; Debian slim usually has bash. An ENTRYPOINT of ["/bin/bash", …] fails on Alpine every time. Switch to /bin/sh or install bash explicitly — never assume "Linux means bash." The same applies to Node images that put npm under a versioned path outside the default PATH for the runtime user.
Checklist
- Is the file actually there?
--entrypoint sh, thenls(use a debug image if the production base has no shell) - There but "not found"?
od -cthe shebang for CRLF (nine out of ten Windows-edited scripts) - Exec-form ENTRYPOINT with
$VAR? Switch to shell form or an entrypoint script - Relative paths resolve against WORKDIR, not the directory you imagined; check the executable bit
exec format error→ architecture mismatch, rebuild with buildx--platform- Missing
bash/npm→ confirm packages and PATH in this base image, not another distro's absolute paths
Triage notes
The four cause groups are ranked by likelihood, but CRLF is separately flagged as the most insidious — it exploits the misdirection of the error itself: when you get "no such file or directory", the file is usually right there and what's missing is the interpreter the shebang points to, so an ls proving the file exists only deepens the confusion. The article says plainly that most tickets end at group one (CRLF) or group four (architecture), which is why the verification command is od -c on the shebang's bytes rather than cat on the contents — precisely to sidestep the "but I can see the script right there" trap.
Provided by ToolVault. Related tools: Dockerfile Generator, Run to Compose, Linux Cheatsheet. Related reading: Docker daemon unreachable, Container name conflicts, No space left on device. See the homepage for more developer tools.
Related Tools
Related Articles
Linux Crontab in Practice: Commands, Logs, and Debugging
Crontab beyond syntax: three must-know commands, /etc/cron.d vs user crontabs, five-step debugging, PATH/timezone/% escaping, MAILTO, and what to do when you need second-level scheduling.
docker run to docker-compose: The Complete Migration Guide (with Flag Map)
Migrate docker run commands to docker-compose.yml: five advantages of compose over raw run, a flag-by-flag mapping table (ports, volumes, env, restart, networks), the classic pitfalls (bare -e passthrough, anonymous volumes, --rm semantics), and an online converter that runs locally in your browser.
Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
git clone or push rejected with Permission denied (publickey) fatal: Could not read from remote repository? Covers missing keygen, key not loaded in the agent, public key not added to GitHub/GitLab, multi-account key routing with ~/.ssh/config, deploy key limits, and wrong remote URLs — with ssh -v diagnostics.