exec /entrypoint.sh: no such file or directory — the script is in the image, but it was saved with Windows line endings. Its first line reads #!/bin/sh plus a carriage return, so Linux looks for an interpreter called sh\r, finds none, and reports the script as missing. Convert the script to LF, rebuild, and the container starts.

To keep it fixed, add *.sh text eol=lf to the repository's .gitattributes, so Git never checks a shell script out with CRLF on Windows.

The error

PS> docker run --name fix5-crlf-bad fix5-crlf:bad
exec /entrypoint.sh: no such file or directory

The container exits with code 255. With a relative entrypoint the same failure reads exec ./entrypoint.sh: no such file or directory. The Dockerfile itself is ordinary:

FROM alpine:latest
ENV APP_NAME=clinic-worker
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["echo", "hello from the container"]

Why it happens

When Linux executes a file that starts with #!, the kernel reads the rest of the first line as the path of the interpreter. A CRLF file keeps the carriage return on that line, which od -c shows inside the image:

PS> docker run --rm --entrypoint sh fix5-crlf:bad -c "head -n 1 /entrypoint.sh | od -c"
0000000   #   !   /   b   i   n   /   s   h  \r  \n
0000013

There is no /bin/sh\r, the exec fails with "no such file or directory", and the runtime prints that error next to the script's name. That sends people looking for a wrong COPY path, while the file is right where it should be. Running the script through sh explicitly only moves the problem: the carriage return becomes part of set -e and the container prints /entrypoint.sh: set: line 2: illegal option -.

The CRLF usually arrives from an editor on Windows or from Git. With core.autocrlf=true, a script stored with LF in the repository was checked out with CRLF in the test, and file said so: "with CRLF line terminators".

The fix

Convert the script in Git Bash, which ships dos2unix, then rebuild:

$ dos2unix entrypoint.sh
dos2unix: converting file entrypoint.sh to Unix format...
$ file entrypoint.sh
entrypoint.sh: POSIX shell script, ASCII text executable
$ docker build -q -t fix5-crlf:good .
$ docker run --name fix5-crlf-good fix5-crlf:good
entrypoint: starting clinic-worker
hello from the container

Then pin the line endings for everyone who clones the repository:

*.sh text eol=lf

With that .gitattributes line committed, a fresh clone with core.autocrlf=true checked the script out with LF. If you cannot control how the build context is checked out, strip the carriage returns inside the image as a second line of defence; it worked on the CRLF script unchanged:

RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh

How it was reproduced

On Windows 11 with Docker Desktop 4.95.0 (engine 29.8.2) and the cached alpine:latest image (Alpine 3.24.2). The four-line entrypoint.sh was written with CRLF on purpose; the image built without complaint and every run failed with exit code 255. The conversion used Git Bash 5.2.26 with dos2unix 7.5.2, and the .gitattributes test cloned a scratch repository twice with Git 2.45.2 and -c core.autocrlf=true, once before and once after the line was added. The names starting with fix5- are the test's own images and containers.

Frequently asked

Why does Docker say no such file or directory when entrypoint.sh exists?
The script has Windows CRLF line endings. The kernel reads the first line as the interpreter path including the carriage return, cannot find /bin/sh followed by a carriage return, and the runtime reports that against the script name.
How do I convert a shell script from CRLF to LF on Windows?
In Git Bash run dos2unix on the file, or switch the file to LF in your editor, then rebuild the image. The file command should no longer mention CRLF line terminators.
How do I stop Git from adding CRLF to shell scripts?
Commit a .gitattributes file with the line *.sh text eol=lf. Git then checks shell scripts out with LF on every machine, whatever core.autocrlf says.

More decoded errors in the Fixes category. Where Git's CRLF conversion comes from, and the warning it prints when it happens: LF will be replaced by CRLF.