Container exits with "exec /entrypoint.sh: no such file or directory": CRLF line endings
A worker image built fine but every container exited at once with "no such file or directory" for a file that clearly existed. A Windows checkout had turned the shebang into /bin/bash with a carriage return. We fixed it with .gitattributes and a CI check.
We run background workers for a logistics platform as containers, each started by a small
entrypoint.sh that waits for the database and then launches the worker. A new team member on
Windows changed the script to add a health file, opened a pull request, and the CI image built
without errors. In the staging cluster every new worker pod went straight to CrashLoopBackOff.
The container log contained one line:
exec /entrypoint.sh: no such file or directory
The file was obviously there. docker run --entrypoint sh into the same image and ls -l showed
it, executable, at the expected path. Running it by hand inside the container produced a different
and more useful message:
# /entrypoint.sh
sh: 1: /entrypoint.sh: not found
# bash /entrypoint.sh
/entrypoint.sh: line 2: $'\r': command not found
/entrypoint.sh: line 9: syntax error near unexpected token `$'do\r''
And on a Linux laptop, running the checked-out script directly gave the classic version:
bash: ./entrypoint.sh: /bin/bash^M: bad interpreter: No such file or directory
Reproducing it
file and cat -A made the invisible visible:
file docker/entrypoint.sh
docker/entrypoint.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
head -2 docker/entrypoint.sh | cat -A
#!/bin/bash^M$
set -euo pipefail^M$
Git confirmed the CRLF was in the repository itself, not just in someone's working tree:
git ls-files --eol docker/entrypoint.sh
i/crlf w/crlf attr/ docker/entrypoint.sh
i/crlf means the committed blob has CRLF line endings, and the empty attr/ means no
.gitattributes rule applied to the file.
What we ruled out
- A missing file in the image.
docker image inspectand a shell in the container both showed/entrypoint.shwith mode0755. - The wrong base image. The base was
debian:bookworm-slim, which has/bin/bash. An Alpine base without bash produces the same message, so it was worth checking. - Permissions. A missing execute bit gives
permission denied, notno such file or directory.
Root cause
The developer's Git for Windows was configured with core.autocrlf=false and their editor saved
the file with Windows line endings. With no .gitattributes in the repository, Git committed the
bytes as they were. The first line became #!/bin/bash\r. The kernel reads the shebang literally
and tried to run an interpreter named /bin/bash followed by a carriage return, which does not
exist, so execve returned ENOENT. Docker reports that as "no such file or directory", naming the
script rather than the missing interpreter.
What is happening under the hood
When you execute a file that starts with #!, the Linux kernel parses the rest of that first line
up to the newline (\n) as the interpreter path plus an optional argument. A carriage return
(\r, shown as ^M) is not a separator, so it becomes part of the path. The lookup fails with
ENOENT, and every layer above the kernel reports that error against the file it tried to run:
| Who runs the script | What you see |
|---|---|
| bash in a terminal | /bin/bash^M: bad interpreter: No such file or directory |
| Docker or containerd entrypoint | exec /entrypoint.sh: no such file or directory |
dash (/bin/sh on Debian) |
sh: 1: /entrypoint.sh: not found |
bash script.sh (interpreter given explicitly) |
Script starts, then $'\r': command not found and syntax errors |
Git can translate line endings, but only when it is told what a file is. core.autocrlf is a
per-machine setting, so its behaviour depends on how each developer installed Git. A
.gitattributes file is committed with the repository and applies to everyone, which is why it
should be the source of truth.
Environment
| Component | Version / value |
|---|---|
| Developer machine | Windows 11, Git for Windows 2.49, core.autocrlf=false |
| Repository | No .gitattributes, no .editorconfig |
| Image | debian:bookworm-slim, ENTRYPOINT ["/entrypoint.sh"] |
| Runtime | containerd on Kubernetes 1.31 (staging), Docker Engine 28 locally |
Minimal reproduction
printf '#!/bin/bash\r\necho hello\r\n' > crlf.sh
chmod +x crlf.sh
./crlf.sh
# bash: ./crlf.sh: /bin/bash^M: bad interpreter: No such file or directory
FROM debian:bookworm-slim
COPY crlf.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
docker build -t crlf-demo . && docker run --rm crlf-demo
# exec /entrypoint.sh: no such file or directory
Who is affected
Any repository with shell scripts, Makefiles or other LF-sensitive files and at least one contributor on Windows. It also hits Docker Desktop users with bind mounts: the image can be fine while a mounted working copy with CRLF overrides the script at runtime. Python, Node.js and PHP scripts with a shebang fail the same way when executed directly.
The fix
1. Declare line endings in the repository. We added a .gitattributes at the root:
* text=auto
*.sh text eol=lf
*.bash text eol=lf
Dockerfile text eol=lf
Makefile text eol=lf
*.env text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.png binary
*.jpg binary
text=auto lets Git normalise any file it detects as text to LF in the repository. The explicit
eol=lf rules additionally force LF in every working tree, including on Windows, for files that
Linux will execute.
2. Renormalise what is already committed. Adding the attributes does not rewrite existing blobs. This command re-stages every tracked file through the new rules:
git add --renormalize .
git status --short
git commit -m "Normalize line endings per .gitattributes"
Developers with existing clones refreshed their working trees once:
git rm --cached -r -q .
git reset --hard
That second step discards uncommitted local changes, so we asked people to commit or stash first.
3. Align editors. An .editorconfig keeps editors from reintroducing CRLF before Git ever
sees the file:
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{bat,cmd,ps1}]
end_of_line = crlf
Alternatives and trade-offs
- Strip carriage returns in the Dockerfile with
RUN sed -i 's/\r$//' /entrypoint.shordos2unix. It makes the image robust but hides the problem: the same script still breaks in CI jobs and on Linux laptops. We kept it out so the CI check below stays meaningful. git config --global core.autocrlf inputon every Windows machine. Correct, but it depends on each person's setup and does nothing for new contributors.- Call the interpreter explicitly (
ENTRYPOINT ["bash", "/entrypoint.sh"]). Avoids the shebang lookup, but bash still chokes on\rinside the script, as the error output above showed.
How to test the fix
git ls-files --eol docker/entrypoint.sh
# i/lf w/lf attr/text eol=lf docker/entrypoint.sh
docker build -t worker:test -f docker/Dockerfile .
docker run --rm worker:test --version
We also tested on a fresh Windows clone with core.autocrlf=true, the Git for Windows installer
default, and confirmed w/lf for the script.
Confirming in production
After merging the normalisation commit, staging pods started normally on the first attempt and the
CrashLoopBackOff alerts cleared within one rollout. The renormalise commit touched 14 files, four
of them shell scripts that had been working only because nobody executed them directly.
A repository-wide check now returns nothing:
git ls-files --eol | grep -E '^i/crlf'
Regression guard
A short CI job fails a pull request if any LF-only file has CRLF in the index:
#!/usr/bin/env bash
set -euo pipefail
bad=$(git ls-files --eol | awk '$1=="i/crlf" && $NF ~ /(\.sh|\.bash|Dockerfile|Makefile)$/ {print $NF}')
if [[ -n "$bad" ]]; then
echo "CRLF line endings found in:"; echo "$bad"; exit 1
fi
We also added a container smoke test to CI that starts the built image with --version, so an
entrypoint that cannot execute fails before deployment rather than in the cluster.
Security implications
None beyond the bug itself; line endings do not change what the script is allowed to do. The
workaround people often reach for, a sed that rewrites scripts during the build, is harmless. Be
more careful with "fix it in the container" approaches that mount writable scripts from the host
into production containers, because those let a host-side change alter what runs as the
entrypoint.
Prevention checklist
- Commit a
.gitattributeswitheol=lffor every file Linux executes or parses strictly. - Run
git add --renormalize .once after adding or changing attributes. - Add an
.editorconfigso editors agree with Git. - Fail CI on CRLF in scripts and smoke-test container entrypoints.
- When you see "no such file or directory" for a file that exists, check the shebang first.
References
- Git documentation,
gitattributes(text, eol) — https://git-scm.com/docs/gitattributes - Git documentation,
git add --renormalize— https://git-scm.com/docs/git-add - Git documentation,
git ls-files --eol— https://git-scm.com/docs/git-ls-files - Linux man-pages,
execve(2)(interpreter scripts) — https://man7.org/linux/man-pages/man2/execve.2.html - Docker documentation, Dockerfile
ENTRYPOINT— https://docs.docker.com/reference/dockerfile/#entrypoint - EditorConfig specification — https://editorconfig.org/
Add a label
Embed this post
<iframe src="https://hibug.me/bin-bash-bad-interpreter-crlf-windows" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I have spent most of my career on the side of software that pages people at night: payment integrations, web platforms with real traffic, CI/CD pipelines, Kubernetes clusters and the databases underneath them. Somewhere along the way I started keeping a notebook of every bug that cost more than an hour — the exact error, what we ruled out, the real root cause and how we proved the fix. Hibug is where those notes live. Every post follows the same path: Investigate, Explain, Fix, Verify — with the environment spelled out, the fix tested, and the security angle checked.
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.