构建上下文与.dockerignore:优化构建效率

刚开始用 Docker 构建那会儿,我总觉得构建速度还行,直到项目越来越大……一次构建居然要等三分钟。后来才发现,罪魁祸首就是构建上下文传了太多无关文件。

什么是构建上下文?说白了,就是 docker build 命令后面那个路径(默认是 .)里的所有东西。Docker 会把整个目录打包发给守护进程,守护进程再从中找 Dockerfile 开始构建。

核心观点:构建上下文的体积,直接影响构建的启动时间和缓存失效概率。

上下文是怎么工作的

执行 docker build -t my-image . 时,客户端会把当前目录所有文件(包括子目录)压缩成 tar 包,发送给守护进程。守护进程解压后,拿到 Dockerfile 和所有文件,开始逐层构建。

你想想看,如果目录里有 node_modules、.git、IDE 配置、编译中间产物……这些对 Dockerfile 里的 COPY/ADD 指令来说,都是不必要的数据。它们不仅让 tar 包变大,还会在 COPY 指令执行时造成缓存失效——哪怕源文件没改,这些无关文件的变更也会触发重新缓存。

我以前一个前端项目,.git 目录就占了 200MB,node_modules 更是 800MB。每次构建光是传输上下文就要 10 秒。后来用了 .dockerignore,瞬间降到 50MB 以内,构建时间缩短了 80%。

认识 .dockerignore

.dockerignore 的语法跟 .gitignore 几乎一样。它告诉 Docker:打包上下文时,忽略以下文件或目录,不要发给守护进程。

嗯,这里要注意,.dockerignore 只影响上下文,不会影响 Dockerfile 里的 RUN 指令在容器内部产生的文件。它只是裁剪发送给守护进程的“原料”。

我的习惯:每个 Docker 项目的根目录都会放一个 .dockerignore 文件,哪怕项目很小。这能养成好习惯,避免将来项目膨胀后踩坑。

编写 .dockerignore 的常见模式

下面列举一些我最常用的规则,你可以直接拿来用:

# 版本控制
.git
.gitignore
.gitattributes

# 依赖安装目录(包管理器)
node_modules
vendor
.bundle

# 构建缓存和中间产物
.cache
dist
build
tmp
*.o
*.pyc
__pycache__

# IDE 和编辑器配置
.idea
.vscode
*.swp
*.swo

# 操作系统文件
.DS_Store
Thumbs.db

# 日志和临时文件
*.log
.env.local
*.pid

# Docker 自身(避免循环)
Dockerfile
.dockerignore

我曾经翻过车:在 .dockerignore 里写了 Dockerfile,结果构建时 Dockerfile 被忽略了,构建直接报错。因为 Dockerfile 必须在上下文中才能被找到。所以 千万不要忽略 Dockerfile!上面示例里我故意写了,但实际生产中要删掉这行。

规则是按行生效的,支持通配符和目录匹配。可以用 ! 取反,比如先忽略所有 *.md,再保留 README.md:

*.md
!README.md

优化构建效率:从上下文出发

除了编写 .dockerignore,还有几个核心优化点:

优化策略说明
最小化上下文只把 Dockerfile 和实际需要的源文件放在一个干净目录里,不要整个项目根目录。docker build -f path/to/Dockerfile . 的 . 可以换成更小的目录。
利用构建缓存把不常变动的层(如安装依赖)写在 Dockerfile 前面,COPY 频繁改动的源文件放在后面。COPY package.json . && RUN npm install 就比先 COPY 所有代码再 npm install 更不容易缓存失效。
使用多阶段构建第一阶段生成编译产物,第二阶段只 COPY 最终产物,上下文里不需要编译器、测试工具等大体积内容。
分层构建上下文如果一个项目有多个服务,考虑每个服务独立构建,而不是在根目录下统一构建。

多说一句,COPY . /app 这种写法在 .dockerignore 配合下才安全。否则一旦项目里混入了大文件,构建速度会直线下降。

实测对比:我维护的微服务项目,加入 .dockerignore 前上下文 1.2GB,构建平均耗时 4 分半;加入后上下文压缩到 120MB,构建降至 45 秒。缓存命中率从 30% 提升到 85%。

SVG:构建上下文过滤流程

下面这张图展示了整个上下文从目录到构建守护进程的过程,以及 .dockerignore 如何过滤文件:

构建上下文过滤流程 项目根目录 .dockerignore 过滤 精简上下文 全部文件 仅需要的文件 node_modules (800MB) .git (200MB) dist (150MB) 🚫 精简上下文 Dockerfile src/ package.json ... 仅必需文件 守护进程构建 构建速度提升 80%

图中红色节点表示被 .dockerignore 排除的大体积目录,它们不会进入构建上下文。最终发送给守护进程的是精简后的文件集合,构建速度自然快得多。

调试与验证

有时候你不太确定 .dockerignore 是否生效了,可以用下面命令查看发送的上下文大小:

docker build --no-cache -t my-image . 2>&1 | grep "Sending build context"

也可以先查看将要发送的文件列表(不实际构建):

tar -tf - < /dev/null  # 没用,更推荐用 docker buildx
docker buildx build --no-cache --progress=plain . 2>&1 | head -20

我的调试技巧:在 Dockerfile 里加一行 RUN ls -la /app 也可以看到 COPY 进来了什么,但那个过程已经建立在上下文发送之后了。如果想在发送前确认,可以用 docker context inspect 之类的方法,不过最简单的还是算一下目录大小对比。

好了,这一章的核心就这些。记住一个原则:只发必要的文件。你下次做 Docker 构建的时候,可以检查一下自己的项目根目录,看看能不能砍掉一些无用的东西。嗯,我相信你们会回来的——因为构建快了,心情就好了。