构建上下文与.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 排除的大体积目录,它们不会进入构建上下文。最终发送给守护进程的是精简后的文件集合,构建速度自然快得多。
调试与验证
有时候你不太确定 .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 构建的时候,可以检查一下自己的项目根目录,看看能不能砍掉一些无用的东西。嗯,我相信你们会回来的——因为构建快了,心情就好了。