LabHub
学习 学习路径 课程

构建镜像

构建还没开始,就已经慢下来了

在 LabHub 中继续学习

一句话总结

docker build . 中的那一个点会把当前目录的全部内容发送给构建器。无论缓存设计得多好,如果要传输 800MB,构建在真正开始之前就已经很慢了。

概念图: 一个点 · 传输仍然会发生。 · 速度 · 缓存

为什么需要它

Dockerfile 只有两行,构建却耗时 40 秒。查看日志第一行,就能找到答案。

Sending build context to Docker daemon  812.4MB

在构建开始前,Docker 会先把整个当前目录压缩并发送给构建器。node_modules.gitdist,甚至昨天下载的转储文件,都会被发送。即使 Dockerfile 根本不用它们,传输仍然会发生。

这会同时破坏两件事。

  1. 速度——每次构建都要传输数百 MB。
  2. 缓存——只要上下文中的任意一个文件发生变化,COPY . . 的缓存就会失效。.git/index 即使只执行 git status 也会改变,因此即使没改源代码,缓存也可能失效。

工作原理

.dockerignore 的语法与 .gitignore 相似,但用途不同.gitignore 表示“不纳入版本控制的内容”,.dockerignore 表示“不发送给构建器的内容”。两者有很多重叠,但并不相同——例如,不能把 .git 写入 .gitignore,却必须把它写入 .dockerignore

.git
node_modules
**/__pycache__
*.log
dist/
.env

最后一行很重要。如果把 .env 留在上下文中,COPY . . 会把它原样写进镜像。即使之后再增加一个删除它的镜像层,它仍然留在前面的层中。

可复现性——同一个 Dockerfile,不同的结果

即使用 .dockerignore 缩小了上下文,构建仍无法复现,通常还有另外两个原因。

浮动标签。 FROM python:3.12 在今天和下个月可能指向不同的镜像。可以固定为 FROM python:3.12.7-slim 这样的版本;若要求更严格,则使用摘要写成 FROM python@sha256:...

软件包索引。 apt-get install curl 会获取当天的最新版本。这是昨天通过的构建今天失败的经典原因;在确实需要稳定的地方,应明确指定版本。

问题 症状 应对方式
上下文过大 “Sending build context” 显示数百 MB .dockerignore
包含 .git 未修改源代码,缓存却失效 .dockerignore 中加入 .git
浮动标签 构建结果与上周不同 固定补丁版本或摘要
秘密混入 镜像中包含 .env .dockerignore + 构建秘密

ARG 会留在镜像中

ARG API_TOKEN
RUN curl -H "Authorization: $API_TOKEN" ...

这样写会使该值留在 docker history 中。构建时秘密不应通过 ARG 传递,而应使用 BuildKit 的秘密挂载。ARG 只用于“版本号”这类可以公开的值。

实际现场中的情况

除了缩小上下文,还有哪些不发送文件的方法

.dockerignore 用于减少要发送的内容,但也可以通过完全不同的途径获取文件。应该选择哪种方式,取决于该文件为什么是构建所需的

仅构建时需要、最终镜像不应包含的内容——这里适合使用前一课程介绍的多阶段构建和秘密挂载。凭据根本不放进上下文,而是通过挂载传入;编译工具和中间产物留在前一阶段,只移动最终结果。

构建期间获取的内容——使用缓存挂载,无需把依赖目录放进上下文,也能在不同构建之间复用。正因如此,不必发送整个 node_modules;上下文变小的同时,缓存效果也会更好。

从远程获取的内容——构建器还可以接收 Git 仓库地址或 tarball 地址,而不是本地上下文。在 CI 这类源代码已经位于当前环境中的场景里,差异不大;但在无法创建上下文的环境中,这是唯一的途径。

此外,最好掌握确认实际发送了什么内容的方法。上下文包含哪些文件,会因 .dockerignore 规则的顺序和否定模式而变得难以判断。镜像构建完成后,检查其中的文件列表,就能立即发现是否混入了意外内容。这也是确认是否包含秘密最可靠的方法。正如前一课程所述,镜像一旦推送到仓库就无法真正收回,因此推送前检查一次非常值得。

下一项检查要关注什么

接下来的测验将要求判断构建上下文边界、.dockerignore、缓存键和秘密传递方式。请根据前一实验中测得的传输大小和缓存失效结果,区分与源代码变更无关的文件会如何影响构建。