KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03 · 构建上下文 — keel 龙骨
类型:主题概念 · 应用课对应:01 章 §三、§四
类型:主题概念 · 应用课对应:01 章 §三、§四
这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。
一、它为什么会出现
构建要按一份 Dockerfile 把文件装进镜像里,那"这些文件从哪儿来"就必须有一个明确的答案。答案就是构建上下文——docker build 后面那个路径。
它之所以经常出问题,是因为"我传了什么"和"我以为我传了什么"是两件事:
① 每次 docker build 都要等四分钟,明明只改了一行注释
② .env / .git / 本地日志一起被传进了构建过程,后来发现镜像里带着密钥
它面对的需求是:构建需要一个"输入目录",但这个目录里不全是该给构建器的东西。
二、它是什么
一句话:构建上下文 = docker build <路径> 里那个路径下的全部文件,被打包、跨进程送给构建器的那一段字节。
三个"不是":
- 不是工作目录。 它的语义不是"在这个目录里执行构建",而是"把这个目录下的文件交给构建器"。
- 不等于源码。 它是这个目录下的所有文件:
node_modules/、.git/、本地日志、编辑器残留、.env。你以为只传了源码,实际上传了开发机的整个现场。 - 不是"传过去之后才处理"的。 打包发生在你敲命令的那一侧(CLI 进程),所以
.dockerignore在上传之前就生效了——被排除的文件根本不进入传输,也就从没出现在构建器视野里。这和"传过去再删掉"是两件事:后者会把内容留在更早的那一层里。
三、它长什么样
它长成一个目录,外加一份排除清单:
app/
├─ src/ ← 要的
├─ package.json ← 要的
├─ node_modules/ ← 不要(容器里会重装)
├─ .git/ ← 不要(版本历史,含完整提交内容)
├─ debug.log ← 不要(本地日志,内容每次都在变)
└─ .dockerignore ← 就是这份清单
# .dockerignore
node_modules
.git
*.log
.env
应用课里有一份实测:同一个目录,加上 .dockerignore 前后,上下文体积从 3337.2 KB 收缩到 1.1 KB。
四、它怎么工作
① 过滤 docker CLI 读 .dockerignore,把该排除的排掉
② 打包 把剩下的文件打成一个 tar 流
③ 传输 ← 唯一跨进程的一跳:这段字节从 CLI 交给构建器
④ 取用 Dockerfile 里的 COPY / ADD 只能从这个上下文里取文件
第 ③ 步是唯一一次跨进程,上下文的大小决定的就是这一跳的成本。
第 ① 步还有一层不那么直观的作用:COPY 产出的那一层,身份取决于被复制文件的内容。所以一个每天都在变的日志文件留在上下文里,代价不只是"多传了几 KB",而是让那一层每次都作废。构建变慢的真正原因常常在这里,而不在 Dockerfile 的指令顺序上。
五、它会带出哪些概念
- 镜像:上下文是造镜像的输入,构建的结果是"层 + config"。→ 00 页
- 层:
COPY进来的内容变一次,那一层的摘要就变一次,它后面的所有层跟着重做。→ 01 页 - 应用课 01 章 整章就是这一页的展开(含安全边界与那份体积实测)。
六、回到应用课
| 应用课里的那句话 | 现在你知道它为什么成立 |
|---|---|
".dockerignore 在上传前生效" |
过滤发生在 CLI 进程里、排在"打包"之前——被排除的东西从未跨进程 |
". 里的东西不等于源码" |
上下文是路径下的全部文件,而不是"源码"这个概念 |
| "上下文大不只慢,还是一个安全边界" | 跨进程送过去的东西,构建器就可能把它写进某一层;而层是不可变的,写进去就一直在 |
这一页改变了你的哪个判断:构建慢或镜像莫名其妙地大的时候,第一件事不是去调 Dockerfile 的指令顺序,而是先问**"这次传过去的目录里都有什么"**——顺序优化只对已经传过去的东西有效。
自测题
- 一个 5 KB 的日志文件留在上下文里,为什么它带来的代价远大于 5 KB?
- "把文件传过去、再在 Dockerfile 里删掉"和"用
.dockerignore排除掉",结果差在哪?