Docker 入口脚本存在却报 no such file or directory
Docker 启动时报入口脚本不存在,文件和执行权限却正常。核对目录挂载、首行 CRLF 与镜像内解释器,修正实际来源文件,并用 Git 属性固定 LF;保留文件备份和服务回退路径。
容器启动后立即退出,日志只留下一行:
exec ./docker-entrypoint.sh: no such file or directory
脚本已经 COPY 进镜像,权限也有执行位,重新构建仍然报错。若项目在 Windows 上检出、运行的是 Linux 容器,应检查脚本的 CRLF 换行,以及运行时的目录挂载。文件能被列出来,不代表内核能找到它首行指定的解释器。
Windows 检出的脚本带 CRLF,项目目录又挂载到 /app 时,镜像中原本使用 LF 的同名文件可能被遮住。要检查容器运行时实际读取的文件,不能仅因镜像内有脚本,就排除挂载与换行问题;同名报错仍需分别确认文件、工作目录和解释器。
下面的命令使用 Bash,适用于本地或测试环境中的 Docker Compose 开发项目;Windows 可以使用 Git Bash 或 WSL。示例中的 app、容器名和 /app/docker-entrypoint.sh 都需要替换成实际值。纯 Windows 容器不适用。
容器启动时读到的可能是宿主机脚本
先查看失败容器的入口、工作目录和挂载,不需要启动应用:
container_name=your_failed_container
docker inspect --type container --format \
'{{json .Config.Entrypoint}} {{json .Config.Cmd}} {{json .Config.WorkingDir}}' \
"$container_name"
docker inspect --type container --format '{{json .Mounts}}' "$container_name"
相对入口 ./docker-entrypoint.sh 要结合工作目录判断。挂载信息里的 Source 是宿主机来源,Destination 是容器目标。如果整个项目挂到 /app,入口又位于 /app,它就会读取挂载后的内容。绑定挂载会遮住目标目录原有的文件,不会把两份同名脚本自动合并。
因此,在 Dockerfile 中转换换行,或反复使用无缓存构建,未必能修复运行时由宿主机提供的脚本。应修改挂载来源那份文件。若没有覆盖入口的挂载,再检查构建上下文、COPY 路径及实际使用的镜像。
在项目根目录检查已被 Git 跟踪的脚本:
git ls-files --eol -- docker-entrypoint.sh
git check-attr text eol -- docker-entrypoint.sh
git ls-files --eol 的 i/ 表示索引里的换行,w/ 表示工作区文件,attr/ 表示属性规则。若索引是 i/lf,工作区却是 w/crlf,挂载进去的仍是工作区那份 CRLF 文件。没有输出时,还要确认路径和跟踪状态,不能据此判断文件已是 LF。
检查首行的实际字节和解释器
已经退出的容器不能用 docker exec 进去。可在测试项目中覆盖入口,创建一个临时检查容器:
docker compose run --rm --no-deps --entrypoint /bin/sh app -c '
pwd
ls -l /app/docker-entrypoint.sh
head -n 1 /app/docker-entrypoint.sh | od -An -t x1
'
这条命令不执行原入口脚本,但会创建容器,并继承该服务的挂载等配置;执行前确认连接的是测试项目,挂载没有生产数据。--no-deps 防止带起依赖服务,--rm 在结束后移除临时容器。镜像必须包含 /bin/sh、head 和 od;distroless 等镜像要采用其对应的调试方法,不能照抄。
检查首行末尾是否是 0d 0a:0d 是 CR,0a 是 LF。例如 #!/bin/sh 后面若带 CRLF,Linux 解析出的解释器路径会保留末尾的 CR,尝试寻找 /bin/sh\r,于是文件存在也会报找不到。Linux 的脚本解析代码只把空格和制表符作为相关分隔符,并没有在这里把 CR 自动当成换行清掉。
#!/usr/bin/env bash 带 CRLF 时,表现可能变成 env 找不到 bash\r。反过来,即使文件已是 LF,#!/bin/bash 指定的 Bash 不在镜像中,也会启动失败。按首行检查容器内的实际解释器,不要直接把 Bash 脚本改成 #!/bin/sh:两种 shell 的语法不一定兼容。
execve 的 ENOENT 条件包括执行路径、脚本解释器或 ELF 解释器不存在。因此,文件缺失、工作目录错误、解释器缺失和 CRLF 要分别确认;chmod +x 不会修正首行的字节。
转换已确认的脚本,保留原文件
如果首行已经确认带 CRLF,修改实际挂载的宿主机文件。编辑器里可把换行设置改为 LF,再保存;不要连带修改项目中所有文件的编码或换行。
也可以在项目根目录用 Python 3 转换这一份普通脚本。下面只替换 CRLF,不删除任意位置的 CR,并在改写前保存备份:
python3 - <<'PY'
from pathlib import Path
import shutil
script = Path('docker-entrypoint.sh')
backup = Path('docker-entrypoint.sh.before-lf')
if script.is_symlink():
raise SystemExit('Refusing to modify a symlink; check the real source file.')
data = script.read_bytes()
if not data.startswith(b'#!'):
raise SystemExit('No leading shebang; check encoding and file type first.')
if b'\r\n' not in data:
raise SystemExit('No CRLF found; file left unchanged.')
if backup.exists():
raise SystemExit('Backup already exists; inspect it before proceeding.')
shutil.copy2(script, backup)
script.write_bytes(data.replace(b'\r\n', b'\n'))
print('Converted CRLF to LF; backup:', backup)
PY
这会覆盖原文件内容,备份要留到验证结束;备份已存在时脚本会停止,不会替你覆盖。它也不处理 UTF-8 BOM、UTF-16 或纯 CR 换行;若首行并非以 #! 开始,应先弄清编码或文件类型。不要对二进制文件使用这个转换脚本。Python 的二进制文件读写可避免文本模式再次转换换行,copy2 会尽量保留文件元数据,但不能当作完整 ACL、所有者和权限备份。
转换后重新读取首行字节,确认只剩 LF,并再次检查解释器存在。如果报错来自另一个 bin/ 脚本,应对那个确切文件做同样检查,而不是批量替换整个目录。
把换行规则保存在仓库中
单次转换后,下一次检出或编辑仍可能带回 CRLF。对于需要在 Linux 容器中执行的脚本,在仓库根目录已有 .gitattributes 中合并规则;没有这个文件时再创建:
docker-entrypoint.sh text eol=lf
*.sh text eol=lf
text 用于文本换行归一化,eol=lf 明确工作区检出时使用 LF。这些属性可按路径设置。只给确实需要 LF 的文件加规则;无扩展名的 binstub 可按实际路径补充,不必把整个仓库都改成同一种换行。
合并后再执行 git check-attr text eol -- docker-entrypoint.sh,确认最终值是 text: set、eol: lf。更深目录的 .gitattributes 或 .git/info/attributes 可能覆盖根目录规则,不能只看根目录写了什么。
先备份未提交改动;脚本若有部分暂存内容,需先保存并梳理,再重新归一化这个已跟踪文件:
git add .gitattributes
git add --renormalize -- docker-entrypoint.sh
git diff --cached -- .gitattributes docker-entrypoint.sh
git ls-files --eol -- docker-entrypoint.sh
--renormalize重新应用归一化规则并更新索引,也会暂存该文件的其他工作区改动,所以要审查差异。它不负责把当前工作区文件重写为 LF;运行时需要的那份文件仍要完成前面的转换。不要用删除重克隆或 reset --hard 代替处理未提交改动。
重新启动原入口并验证
先重跑临时检查容器,确认挂载后的首行已经是 LF。随后恢复正常入口,在测试项目中启动对应服务。服务有 build: 且脚本复制进镜像时,可用:
docker compose up -d --build --no-deps app
这会构建并按需重建服务容器,可能中断该服务。部署前保存现有 Compose 配置、可回退镜像和相关数据备份;依赖服务必须已经按项目要求就绪。对于仅使用预构建 image: 的服务,--build 不会替你修改镜像中的文件,需要重新制作镜像或使用项目已提供的修复版本。
只用绑定挂载提供脚本时,修改来源文件后,新进程会读取修改后的内容;若脚本仍被其他挂载覆盖,重建同一镜像也没有帮助。验证时检查原入口不再报 ENOENT、容器没有再次退出,以及应用真正完成初始化。临时 shell 能启动只证明检查路径可用,不证明原入口和应用已经恢复。
需要回退时,在确认没有新的文件修改后,从 docker-entrypoint.sh.before-lf 恢复脚本内容;.gitattributes 只撤销本次添加的规则,保留原有配置。若更新过镜像,恢复先前保留的镜像与 Compose 配置,再验证业务。恢复 CRLF 备份可能带回原故障,因此备份主要用于撤销误改,不能替代能正常启动的上一版。


