构建 5 秒钟就完成,镜像只有 20MB,容器一启动就退出——这一切的罪魁祸首,竟然是一个看不见的字符。
引言
前几天,我在将一个 Go 项目(example-project)打包成 Docker 镜像时,遇到了一个极其诡异的 Bug:
docker build执行了不到 1 分钟就”成功”完成- 生成的镜像只有不到 20MB(正常应该 50-100MB)
- 容器启动后立即退出,
docker logs查询日志——一片空白,什么都没有
更诡异的是,同样的代码在本地编译运行完全正常。问题到底出在哪里?
经过一整天的排查,我发现罪魁祸首竟然是——一个看不见的字符。
现象:构建快得不正常
先看正常的构建过程,一个完整的 Dockerfile 多阶段构建通常包含:
- 前端依赖安装(npm install)
- 前端项目构建(npm run build)
- Go 依赖下载(go mod download)
- Go 项目编译(go build)
每一步都需要时间,正常构建应该在 5-10 分钟左右,镜像大小在 50-100MB。
但这次我看到的却是:
$ sudo docker build -t example-project:latest .
[+] Building 0.8s (5/5) FINISHED
=> [internal] load build definition from Dockerfile
=> [internal] load metadata for docker.io/library/golang:alpine
=> CACHED [builder2 1/1] FROM golang:alpine
=> exporting to image
=> naming to docker.io/library/example-project:latest
0.8 秒! 构建过程只有 5 个步骤,所有的 RUN、COPY、ADD 指令全部被跳过了。
最终生成的镜像里,没有任何应用代码,也没有编译好的二进制文件。
排查过程:日志一片空白,容器无法启动
第一步:检查容器状态
$ sudo docker ps -a | grep example-project
CONTAINER ID IMAGE STATUS
abc123def456 example-project:latest Exited (0) 5 seconds ago
状态是 Exited,说明容器确实启动了,但很快就退出了。
第二步:查看容器日志
$ sudo docker logs abc123def456 --tail 100
# 没有任何输出
一片空白。 这不符合常理:即使应用启动失败,至少也应该有错误日志输出。
第三步:进入容器查看
$ sudo docker run -it --rm --entrypoint /bin/sh example-project:latest
/go # ls -la
total 16
drwxrwxrwt 4 root root 4096 Jun 16 00:19 .
drwxr-xr-x 1 root root 4096 Jun 30 01:58 ..
drwxrwxrwt 2 root root 4096 Jun 16 00:19 bin
drwxrwxrwt 2 root root 4096 Jun 16 00:19 src
/go # ls -la /app/
ls: /app/: No such file or directory
真相大白了:容器里根本没有应用文件。镜像中的 /app/ 目录不存在,更没有 example-project 二进制文件。
这说明 Dockerfile 中的 RUN go build 等指令根本就没有执行。
根本原因:看不见的换行符
Dockerfile 的解析机制
Docker 在解析 Dockerfile 时,对换行符有严格要求:
- 每一条指令必须以 LF(Line Feed,即
\n) 结尾 - 如果存在 CRLF(Carriage Return + Line Feed,即
\r\n),Docker 会把\r当作命令的一部分进行解析
为什么会出现 CRLF?
在 Windows 系统上,文本文件的默认换行符是 CRLF(\r\n),而在 Linux/macOS 上默认是 LF(\n)。
如果开发者在 Windows 上编辑了 Dockerfile,然后提交到 Git,再在 Linux 服务器上构建——换行符问题就此诞生。
如何验证?
用 cat -A 查看 Dockerfile 的隐藏字符:
$ cat -A Dockerfile | head -10
FROM node:18 AS builder$
$
WORKDIR /web$
$
COPY web/default/package*.json /web/default/$
COPY web/default/ /web/default/$
正常情况下,每行末尾只有一个 $(表示 LF)。但如果有 CRLF,会显示为 ^M$:
FROM node:18 AS builder^M$
^M$
WORKDIR /web^M$
^M$ 就是 CRLF 的标志。
后果:Docker 静默跳过关键指令
当 Dockerfile 中包含 CRLF 换行符时,Docker 的解析器会:
| 换行符类型 | Docker 解析结果 | 后果 |
|---|---|---|
LF(\n) | RUN go build ... | 正常执行 |
CRLF(\r\n) | RUN go build ...\r | 无法识别,静默跳过 |
最关键的是——Docker 不会报错。它只是无法识别这个指令,然后继续执行下一条。所有 RUN、COPY、ADD 指令都被静默忽略,只有 FROM 指令被正确执行。
于是:
- 构建只拉取了基础镜像(
FROM node:16、FROM golang:alpine、FROM alpine:latest) - 没有任何应用代码被复制进去
- 没有任何编译操作被执行
- 最终生成的镜像≈基础镜像的简单堆叠
这就是为什么构建只需 0.8 秒,镜像只有 20MB 的原因。
经验教训与解决方案
预防措施
1. Git 配置
在 .gitattributes 文件中强制使用 LF:
# 强制所有文本文件使用 LF
* text=auto eol=lf
# 针对 Dockerfile 特别指定
Dockerfile text eol=lf
*.dockerfile text eol=lf
2. 编辑器设置
VS Code:
{
"files.eol": "\n"
}
Vim:
:set ff=unix
IntelliJ IDEA:
Settings → Editor → Code Style → Line separator → Unix and macOS (\n)
3. CI/CD 自动化检查
在构建流程中加入换行符检查和自动修复:
# 检查是否包含 CRLF
if grep -q $'\r' Dockerfile; then
echo "Error: Dockerfile contains CRLF line endings"
exit 1
fi
# 或自动修复
sed -i 's/\r$//' Dockerfile
修复方法
如果已经遇到了问题,可以用以下方法修复:
方法一:使用 dos2unix
sudo apt-get install dos2unix -y
dos2unix Dockerfile
方法二:使用 sed
sed -i 's/\r$//' Dockerfile
方法三:在 Vim 中转换
vim Dockerfile
:set ff=unix
:wq
验证修复结果
# 确认没有 ^M 字符
cat -A Dockerfile | head -5
# 重新构建
sudo docker build --no-cache -t example-project:latest .
总结
| 症状 | 正常情况 | 换行符问题 |
|---|---|---|
| 构建耗时 | 5-10 分钟 | < 1 分钟 |
| 构建步骤数 | 10+ 个 | 仅 3-5 个 |
| 镜像大小 | 50-100MB | < 20MB(仅基础镜像) |
RUN 指令输出 | 有 Running in ... 日志 | 完全看不到 |
| 容器状态 | 正常运行 | 立即退出 |
docker logs | 有应用日志 | 无任何输出 |
一句话总结:
Dockerfile 中的 Windows 换行符(CRLF)导致 Docker 静默跳过所有构建指令,最终生成一个空壳镜像,容器一启动就退出,且没有任何日志可供排查。
这个 Bug 让我整整排查了一天。希望看到这篇文章的你,能避开这个坑。😊
构建完成后,记得检查换行符:
cat -A Dockerfile | head -5
# 只应该有 $,不应该有 ^M$