← 全部笔记

Docker · 2 镜像与 Dockerfile

改动从哪一行开始,
重建就从哪里向后传

在同一条构建链上,第一个缓存失配点及其后续步骤需要重建;前面的结果仍能复用。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · 官方文档核对,Docker 实验未实测

构建方向 ↑ 从底部第 1 行向上读

先猜受影响的行,再点击修改;每次只改一处。

修改 src/ 中的文件内容:第 5 行首次失配,第 6 行随之重建。第 1–4 行可复用,依赖不用重装。

图 1 红色实线标出第一个失配点,红色虚线标出后续重建;图示为缓存依赖模型,不表示层大小或耗时。

已有基础:镜像、容器与仓库。本页沿用此前的 FROM、RUN、COPY、WORKDIR;下文当场说明 Python 示例的命令。

01 · 机制

先找首个失配点

构建缓存(build cache)保存已完成步骤的结果。再次构建时,构建器先检查能否直接复用。命中缓存,就跳过这一步的执行。Docker 常用的构建器叫 BuildKit。

你可以把每一步理解为:输入相同,才有机会复用旧结果。输入包括前一步的结果、当前指令,以及这一步读取的文件。前一步一旦变化,后面的步骤即使命令没变,也接不上原来的构建链。[1][2]

前一步结果
+ 当前指令
+ 本步输入文件
→ 检查是否匹配 →
输入改变
× 无法沿用旧结果
→ 传给下一步 →
后续步骤重新构建
图 2 当前指令没变,也可能因为前一步的结果变了而无法命中缓存。

首图只改第 4 行的安装命令,第 3 行与第 6 行分别能否复用?先说明理由,再回首图点击第 4 行。

第 3 行在改动之前,仍可复用。第 4 行指令变了,首先失配;第 5、6 行依赖它的结果,需要重建。只检查“本行有没有改字”会漏掉这条依赖。

边界:这张链图省略了什么?

图中假设单阶段、线性依赖、普通 COPY,没有缓存被清理,基础镜像及构建设置保持一致,也没有命中新分支的其他已有缓存。缓存“失效”表示本次输入无法沿用原链,不代表删除旧缓存。

构建步骤与镜像中的文件系统层不必一一对应。WORKDIR 等指令还会影响构建状态;后面的多阶段构建会出现分支依赖,不能把所有文本位置靠后的步骤都算作同一条链。

02 · 输入

COPY 要校验文件

COPY 的文字不变,输入文件变了,缓存仍可能失配。校验和(checksum)是用来比较输入是否一致的摘要。COPY 会校验涉及的文件内容及相关元数据,例如权限;文件的修改时间 mtime 不参与这项校验。只运行 touch,内容和权限不变时,不会仅因时间改变而失效。[2][3]

固定父步骤与 COPY 指令,单独改变一项输入
变化COPY 的比较结果后续 RUN
源文件内容改变× 失配,重建↳ 跟着重建
源文件权限改变× 相关元数据变了↳ 跟着重建
只改 mtime= 仍可命中= 仍可命中
修改 COPY 没读取的文件= 本步输入不变= 仍可命中
图 3 缓存比较的是本步的输入;目录里有变化,不等于每条 COPY 都要重建。

保持首图不变,只修改 src/main.py 内容。为什么第 3 行的 COPY 不受影响?只 touch 这个文件又会怎样?

第 3 行只读取 requirements.txt,没有读取 src/main.py。内容变化到第 5 行才被发现。如果只改 mtime,且其他输入不变,第 5 行也仍能命中缓存。

普通 RUN 的缓存不会自动检查外部软件仓库有没有新版。父步骤与安装命令不变时,隔天构建也可能跳过安装。缓存命中保证复用旧结果,不保证软件已经更新。[2]

原缓存仍在,其他输入不变。哪项会让首图第 3 行首先失配?

touch 只改修改时间时,mtime 不参与 COPY 的校验。

第 3 行只读取本地文件;普通 RUN 的缓存也不会主动探测远端更新。

依赖版本文字是文件内容。它变了,第 3 行的输入校验失配,第 4 行及其后续步骤重建。能否复用必须沿依赖链判断。

03 · 排序

先装依赖,再放源码

让频繁变化的源码靠后进入构建链。依赖是程序需要的第三方软件包。示例中 requirements.txt 列出 Python 软件包,pip install -r 根据这份清单安装;python -m compileall -q src 编译源码目录里的 Python 文件。[4]

安装所需的文件必须先复制进去。把源码复制推迟到安装之后,就能让“改业务代码”绕开耗时的安装步骤。以下两列共用相同的 FROM 和 WORKDIR。

一开始就复制全部

  1. COPY . .× 源码变化,首先失配
  2. RUN pip install -r requirements.txt↳ 重装依赖
  3. RUN python -m compileall -q src↳ 重新编译

把输入拆开复制

  1. COPY requirements.txt .= 依赖清单未变,可复用
  2. RUN pip install -r requirements.txt= 安装结果可复用
  3. COPY src/ ./src/× 源码变化,从这里失配
  4. RUN python -m compileall -q src↳ 重新编译
图 4 两列均向下执行;只改源码时,拆开 COPY 能保住依赖安装的缓存。

同事把安装放到复制源码之前,却写成 RUN pip install 后才 COPY requirements.txt。为什么这个调整不成立?依赖清单也改了时能省下安装吗?

安装需要读取清单,必须先 COPY requirements.txt,再 RUN 安装,最后 COPY 源码。清单改变时安装的输入变了,应重新安装。

  • 安装之前要有依赖清单。
  • 源码不应提前进入安装步骤的父结果。
  • 依赖本身改变时,需要重装。
深入一点:只有一份清单就够了吗?

这个示例假设安装只依赖 requirements.txt。实际项目可能还要锁文件(精确记录依赖版本的文件)、本地包、安装脚本或配置。把安装需要的输入一起提前复制;遗漏输入会改变构建语义。层缓存也不同于 pip 等包管理器自己的下载缓存,后者不在本页展开。

04 · 过滤

忽略无关文件

.dockerignore 在文件进入构建上下文前做过滤。构建上下文(build context)是本次构建可以读取的文件集合。运行 docker build . 时,末尾的点指定当前目录为上下文根目录,规则文件就放在这个根目录。[5]

COPY . . 会把上下文中可复制的文件放到当前工作目录。若把日志和临时文件也算进去,每次变化都可能触发复制步骤。排除不需要的文件,还能减少传给构建器的数据。[4][5]

.git
__pycache__
**/*.pyc
**/*.log
.env
  • requirements.txt→ 保留 → COPY 可读取
  • src/main.py→ 保留 → COPY 可读取
  • debug.log× 被 **/*.log 排除
  • .env× 被 .env 排除
图 5 被排除的日志内容变化,不会成为 COPY 的新输入;必要的源码和依赖清单仍要保留。

Dockerfile 有 COPY . .,规则里已有 **/*.log。改 debug.log 后会重建吗?如果再把 requirements.txt 写进忽略规则,会发生什么?

已忽略的 debug.log 不参与 COPY 输入,其他条件不变时可复用。若排除 requirements.txt,显式 COPY requirements.txt . 会找不到源文件而失败。忽略规则必须服从构建所需的输入。

.gitignore 不会自动替你过滤本地 Docker 构建上下文。修改 .dockerignore 规则本身,可能改变哪些文件被复制;不要把它理解为“改什么都不重建”的开关。

深入一点:例外规则与敏感文件

** 可匹配任意层级目录,! 可重新包含某类文件;多条匹配时以最后一条为准。别把密钥放进镜像,再指望后面 RUN 删除能抹掉旧层中的副本。这里排除 .env 是为了避免把本地配置随 COPY 带进去,不是完整的密钥管理方案。

05 · 动手

连续构建看差异

这个实验用复制文件模拟“安装依赖”,避免下载真实依赖包干扰观察。它仍然具备“清单 → 安装结果 → 源码 → 产物”的依赖链。使用 BusyBox 镜像内的 shell 工具即可完成。

未实测

编写环境没有 Docker。下面是可运行命令和按官方规则推导的预期现象,尚待维护者在 Docker Engine 29.x 上补实测;没有粘贴虚构的构建日志或终端输出。

在有 Docker 权限的 Linux 终端执行。首次构建需要联网拉取基础镜像;实验创建临时目录、本地镜像和构建缓存。容器不发布端口、不挂载宿主目录,未使用特权参数。

1. 准备独立目录

CACHE_LAB_DIR=$(mktemp -d)
cd "$CACHE_LAB_DIR"
mkdir src
printf 'deps-v1\n' > deps.txt
printf 'source-v1\n' > src/message.txt
printf 'log-v1\n' > debug.log
cat > .dockerignore <<'EOF'
**/*.log
Dockerfile
.dockerignore
EOF
cat > Dockerfile <<'EOF'
FROM busybox:1.37
WORKDIR /app
COPY deps.txt .
RUN cp deps.txt installed.txt
COPY . .
RUN cat src/message.txt > result.txt
CMD ["cat", "/app/result.txt"]
EOF

cp 把依赖清单复制成 installed.txt,只模拟安装结果,不安装软件。最后一条 RUN 把源码内容写到产物 result.txt;CMD 让容器默认显示这个产物。Dockerfile 与忽略文件仍会交给构建器解析,但按这里的规则不会被 COPY 进镜像。

2. 建立并复用缓存

docker build --progress=plain --load -t cache-lab:issue15 .
docker build --progress=plain --load -t cache-lab:issue15 .
docker run --rm cache-lab:issue15

预期:第一次建立结果,第二次的 COPY、RUN 步骤可出现 CACHED。--progress=plain 使用文本构建日志,--load 把结果载入本地镜像库。容器应输出输入文件中的一行文字 source-v1。这是预期值,未实测;首次日志与步骤编号以你的环境为准。[6]

3. 每次只改一个输入

以下命令在同一目录内依次执行。每次构建后,先看 RUN cp deps.txt installed.txt 是否标为 CACHED,再看复制源码和生成产物的步骤。

# A:改源码内容
printf 'source-v2\n' > src/message.txt
docker build --progress=plain --load -t cache-lab:issue15 .
docker run --rm cache-lab:issue15

# B:仅改变源码的 mtime
touch src/message.txt
docker build --progress=plain --load -t cache-lab:issue15 .

# C:改已经被忽略的日志
printf 'log-v2\n' > debug.log
docker build --progress=plain --load -t cache-lab:issue15 .

# D:改依赖清单
printf 'deps-v2\n' > deps.txt
docker build --progress=plain --load -t cache-lab:issue15 .
docker run --rm cache-lab:issue15 cat /app/installed.txt
预期观察清单(规则推导,均未实测)
操作首个失配点应观察到的结果
A 改源码COPY . .模拟安装仍命中;生成产物重跑;运行输出应为 source-v2。
B 只 touch无COPY、RUN 仍可命中。
C 改日志无日志被排除,COPY、RUN 仍可命中。
D 改清单COPY deps.txt .模拟安装及后续重建;检查 installed.txt 应得到 deps-v2。

先不执行 D:如果清单变了,但生成产物的 RUN 命令没改,它能沿用原链的缓存吗?做完后对照日志解释。

不能。COPY deps.txt . 首先失配,模拟安装重新运行;后面的 COPY 和 RUN 都接收到变化后的父结果。命令相同不足以保证缓存命中。

  • 指出第一个失配点。
  • 提到后续步骤依赖父结果。
  • 用实际日志确认,区分预期与实测。
没有看到预期缓存时,怎么查?

确认用了同一个构建器、同一目录和同一基础镜像,且没有运行 --no-cache 或清理缓存。先找到第一个未命中步骤,再检查它实际读取的文件;若命令报错,先解决构建错误,不能把失败当作缓存失效的证据。

实验结束可执行 docker image rm cache-lab:issue15 删除实验镜像,临时目录可在退出该目录后自行删除。构建缓存仍可能保留;本实验不要求清理整个构建器的缓存。

06 · 回顾

沿输入往后检查

你已采用“复制清单 → 安装 → 复制源码”。只改源码,却发现安装也重跑了。哪项最能解释这个现象?

RUN 可以直接复用缓存,实验第二次构建就是用来观察这一点。

mtime 不参与 COPY 校验,后面的源码变化也不会倒过来使前面的安装失效。

合理排序为复用创造条件,还需要缓存实际存在。更换构建器或清理缓存后,原安装结果可能无法使用;检查第一个失配步骤及缓存环境,别只盯源码。

想排除 debug.log,应该在构建上下文根目录的哪个文件中写规则?

文件名:

.dockerignore 在构建器读取 COPY 输入前排除匹配的文件。只把日志写进 .gitignore 不会自动过滤本地构建上下文。

需要强制刷新缓存怎么办?

--no-cache 跳过构建步骤的缓存。它不会自动获取更新的基础镜像;--pull 才会尝试拉取所引用的镜像。需要同时刷新两者时,用 docker build --pull --no-cache -t cache-lab:issue15 .。[6]

来源

以下为 Docker 官方文档,核对日期 2026-11-03;本页面向 Docker Engine 29.x。交互为明确限定条件的教学模型,实验未实测。

  1. Docker build cache:缓存复用与后续依赖。
  2. Build cache invalidation:文件校验、mtime 与普通 RUN 的规则。
  3. Dockerfile reference — COPY:复制源与构建上下文。
  4. Optimize cache usage in builds:排序、拆开依赖输入、缩小上下文。
  5. Build context — .dockerignore files:规则位置、匹配与例外。
  6. docker buildx build:--progress、--load、--no-cache、--pull。