构建 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 个步骤,所有的 RUNCOPYADD 指令全部被跳过了

最终生成的镜像里,没有任何应用代码,也没有编译好的二进制文件。


排查过程:日志一片空白,容器无法启动

第一步:检查容器状态

$ 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(\nRUN go build ...正常执行
CRLF(\r\nRUN go build ...\r无法识别,静默跳过

最关键的是——Docker 不会报错。它只是无法识别这个指令,然后继续执行下一条。所有 RUNCOPYADD 指令都被静默忽略,只有 FROM 指令被正确执行。

于是:

  1. 构建只拉取了基础镜像(FROM node:16FROM golang:alpineFROM alpine:latest
  2. 没有任何应用代码被复制进去
  3. 没有任何编译操作被执行
  4. 最终生成的镜像≈基础镜像的简单堆叠

这就是为什么构建只需 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$