01 · 机制
先找首个失配点
构建缓存(build cache)保存已完成步骤的结果。再次构建时,构建器先检查能否直接复用。命中缓存,就跳过这一步的执行。Docker 常用的构建器叫 BuildKit。
你可以把每一步理解为:输入相同,才有机会复用旧结果。输入包括前一步的结果、当前指令,以及这一步读取的文件。前一步一旦变化,后面的步骤即使命令没变,也接不上原来的构建链。[1][2]
+ 当前指令
+ 本步输入文件
× 无法沿用旧结果
首图只改第 4 行的安装命令,第 3 行与第 6 行分别能否复用?先说明理由,再回首图点击第 4 行。
第 3 行在改动之前,仍可复用。第 4 行指令变了,首先失配;第 5、6 行依赖它的结果,需要重建。只检查“本行有没有改字”会漏掉这条依赖。
边界:这张链图省略了什么?
图中假设单阶段、线性依赖、普通 COPY,没有缓存被清理,基础镜像及构建设置保持一致,也没有命中新分支的其他已有缓存。缓存“失效”表示本次输入无法沿用原链,不代表删除旧缓存。
构建步骤与镜像中的文件系统层不必一一对应。WORKDIR 等指令还会影响构建状态;后面的多阶段构建会出现分支依赖,不能把所有文本位置靠后的步骤都算作同一条链。
02 · 输入
COPY 要校验文件
COPY 的文字不变,输入文件变了,缓存仍可能失配。校验和(checksum)是用来比较输入是否一致的摘要。COPY 会校验涉及的文件内容及相关元数据,例如权限;文件的修改时间 mtime 不参与这项校验。只运行 touch,内容和权限不变时,不会仅因时间改变而失效。[2][3]
| 变化 | COPY 的比较结果 | 后续 RUN |
|---|---|---|
| 源文件内容改变 | × 失配,重建 | ↳ 跟着重建 |
| 源文件权限改变 | × 相关元数据变了 | ↳ 跟着重建 |
| 只改 mtime | = 仍可命中 | = 仍可命中 |
| 修改 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。
一开始就复制全部
COPY . .× 源码变化,首先失配RUN pip install -r requirements.txt↳ 重装依赖RUN python -m compileall -q src↳ 重新编译
把输入拆开复制
COPY requirements.txt .= 依赖清单未变,可复用RUN pip install -r requirements.txt= 安装结果可复用COPY src/ ./src/× 源码变化,从这里失配RUN python -m compileall -q src↳ 重新编译
同事把安装放到复制源码之前,却写成 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 排除
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 · 回顾
沿输入往后检查
- 先找首个失配点,再沿同一条依赖链检查后续步骤。
- COPY 还看文件内容和相关元数据;仅 mtime 变化不算。
- 先复制安装所需的输入,再安装,最后复制频繁变化的源码。
- 用 .dockerignore 排除无关输入,保留构建必需文件。
你已采用“复制清单 → 安装 → 复制源码”。只改源码,却发现安装也重跑了。哪项最能解释这个现象?
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。交互为明确限定条件的教学模型,实验未实测。
- Docker build cache:缓存复用与后续依赖。
- Build cache invalidation:文件校验、mtime 与普通 RUN 的规则。
- Dockerfile reference — COPY:复制源与构建上下文。
- Optimize cache usage in builds:排序、拆开依赖输入、缩小上下文。
- Build context — .dockerignore files:规则位置、匹配与例外。
- docker buildx build:--progress、--load、--no-cache、--pull。