docker 构建缓存陈旧速查:改了代码镜像没变的分层排查
更新时间:2026-10-24。FAQ 速查页,面向「代码明明改了、构建明明显示成功、跑起来还是旧逻辑」的同学——build 输出一排
CACHED,镜像 ID 和昨天一模一样。docker 构建缓存陈旧的本质一句话:缓存不是「内容变了才重建」,而是「缓存键没变就复用」——键由指令文本、父层 digest、COPY 源文件的校验和构成,凡是不在键里的变化(构建参数、上下文外文件、基础镜像的新 digest)都不会触发重建。病灶收敛在六根轴上。缓存之外「构建慢」的问题与「偶发失败」的问题分别看 docker 构建拉取偶发失败速查;语法层看 docker 构建语法速查。
本文要回答的问题
- 改了源码,为什么
COPY . /app这层还是CACHED? --build-arg VERSION=1.2传了新值,镜像里的版本号为什么没变?--no-cache全量重建能好,但有没有不牺牲全部缓存的精准打法?- 多阶段构建选了
--target,为什么产物还是旧的? - Dockerfile 指令顺序怎么排,才能让缓存命中最大化、失效最小化?
一、报错原文与典型现象
缓存陈旧不报错,它的指纹是「该变的没变」——构建日志里大面积的 CACHED,或运行期行为与源码对不上(路径以 /proj/demo 占位,镜像源用 registry.example.com 占位):
# 指纹一:COPY 层误命中 —— 上下文被 .dockerignore 裁掉,源变化没进键
$ docker build -t registry.example.com/team/app:1.1 /proj/demo
[4/7] COPY . /app CACHED
[5/7] RUN make build CACHED # 源码变了,产物却是缓存里的旧二进制# 指纹二:build-arg 不参与后续层缓存键的误解
$ docker build --build-arg APP_VERSION=1.2 -t app .
[6/7] RUN echo "building..." CACHED # 该层没引用 APP_VERSION,ARG 变了也复用# 指纹三:运行期行为与源码不符 —— 最痛的表现形式
$ docker run --rm registry.example.com/team/app:1.1 version
app version 1.0 # 源码里早就是 1.1 了读现象抓三条判据:先看 build 日志里 CACHED 出现在哪一层——从那一层往下全部不可信;CACHED 出现在 COPY . /app 而源码确实改了,第一嫌疑是 .dockerignore 或上下文目录指错;凡是「加 --no-cache 就好」的故障,根因一定在缓存键的构成上,而 --no-cache 只是止血不是治病。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| 改了代码 COPY 层 CACHED | .dockerignore 排除了源码 | docker build 看上下文大小 | 修 .dockerignore,放行源码 |
| 改了代码仍旧逻辑 | 上下文目录指错 | build 末尾看传输字节数 | 修正 build 上下文路径 |
| build-arg 变了产物没变 | 变化点之后的层未引用 ARG | 审 Dockerfile ARG 使用位置 | 在需要失效处引用该 ARG |
| FROM 永远是旧基础镜像 | 本地缓存的 digest 未刷新 | docker inspect 看父镜像 | build 加 --pull 刷新基础镜像 |
| --target 产物是旧的 | 目标阶段上游层缓存误命中 | 逐段构建各 target 对比 | 清相关层缓存重建 |
| CI 每次全量构建 | 远程缓存未配置/未命中 | 日志看 cache-from 命中 | 配 registry 缓存导出导入 |
| 装依赖层每次重建 | COPY 全量在装依赖之前 | 看层序:依赖文件先 COPY | 依赖清单先拷、源码后拷 |
| 同名 tag 覆盖旧镜像 | tag 策略与缓存认知混淆 | docker images 看创建时间 | 唯一 tag 或 digest 部署 |
三、排查决策图
这张图是标准排查路线:红是陈旧现象与根因,蓝是排查动作,绿是验证通过,橙是修复,紫是防复发沉淀。
读这张图抓住关键一步:从第一个误命中的 CACHED 往下全都不信——缓存是链式的,一层错命中,后续每层的「新」都是假象。修复的目标不是「禁用缓存」,而是「让变化进缓存键」:源码进上下文、ARG 在需要失效的层被引用、基础镜像用 --pull 刷新。
四、逐条排查展开
.dockerignore 掏空上下文是 COPY 误命中的首因。 COPY . /app 的缓存键包含上下文里每个文件的校验和;但如果 .dockerignore 把 src/ 排除了(常见于心急复制网上的「通用模板」),源码变化根本不在键里,COPY 层永远命中缓存。验证:构建日志开头 transferring context 的字节数——源码几万行只传了几百 KB,就是被裁了。顺带反向坑:.git/、node_modules/ 没排除,上下文几个 GB,每次构建传输慢且缓存键易碎。
构建参数只在「引用它的层」参与缓存键。 这是最易误解的语义:
ARG APP_VERSION=dev
RUN echo "no use of version" # 不引用 APP_VERSION:ARG 变了这层仍 CACHED
RUN echo "version ${APP_VERSION}" # 引用了:ARG 变化导致这层及以下重建想让某个 ARG 控制缓存失效,就在目标层显式引用它(哪怕只是 RUN echo "bust: ${CACHE_BUST}")。反之,不想让频繁变化的 ARG 炸掉全部缓存,就别在早期层引用它。
基础镜像的 digest 不会自动刷新。 FROM registry.example.com/base/debian:12 第一次构建后,本地存着当时的 digest;上游 base 镜像修复了漏洞重新推了同名 tag,本地重建依然基于旧 digest——docker build 默认不检查远端。处置:构建加 --pull(每次构建前刷新基础镜像),CI 上应作为默认开关。验证当前用的 digest:docker buildx imagetools inspect 对比远端与本地。
多阶段 --target 只构建到目标阶段,但缓存语义不变。 选错 target 会得到「能跑但缺料」的镜像;更隐蔽的是上游阶段的缓存误命中传导到目标阶段。排查法:分阶段单独构建(--target builder、--target final)对比产物,定位误命中发生在哪一段。
层序决定缓存的命中率与失效范围。 黄金法则是「变化频率从低到高排层」:
FROM registry.example.com/base/debian:12
RUN apt-get update && apt-get install -y build-essential # 最稳:系统依赖
COPY go.mod go.sum /app/ # 次稳:依赖清单
RUN cd /app && go mod download # 装依赖(清单变才重建)
COPY . /app # 最易变:源码
RUN cd /app && make build # 编译(源码变才重建)反模式是 COPY . /app 提前——任何文件变动都导致装依赖层失效,每次构建全量装依赖,慢且让人觉得「缓存坏了」。
CI 远程缓存要显式配置。 本地 BuildKit 有层缓存,CI 的干净 runner 没有——要么每次全量构建(慢但结果正确),要么配 --cache-from type=registry,ref=...:buildcache 与 --cache-to 把缓存导出到 registry。注意远程缓存命中失败不会让构建出错,只会让构建变慢——CI 突然变慢先查缓存命中率,别先怀疑网络。
五、规范做法与预防
- 层序进 Dockerfile 评审:依赖清单先拷、源码后拷的序列为模板标配,违反即打回。
- CI 构建默认 --pull:基础镜像 digest 每次刷新,安全修复不隔夜。
- .dockerignore 与 .gitignore 同步维护:排除
.git、构建产物、依赖目录,但绝不排除源码。 - ARG 引用位置有注释:控制缓存失效的 ARG 旁注明「此引用用于让缓存随版本失效」。
- 部署用 digest 不用浮动 tag:
image@sha256:...钉死,「同名 tag 覆盖导致的新旧不分」从根上消失。 - 止血 --no-cache 后必须回头查键:允许紧急用,但故障单上要回答「为什么键没抓住变化」。
这一篇在知识体系里的位置
本篇是 docker 排错速查簇的构建缓存篇,定位「构建成功但产物不对」:构建时好时坏的外部因素看 docker 构建拉取偶发失败速查;Dockerfile 文本层错误看 docker 构建语法速查;compose 文件的解析问题看 docker compose 配置解析失败速查;镜像内容缺料看 docker 镜像缺依赖速查;构建资源耗尽看 docker 资源耗尽速查。层序优化、缓存键审计、digest 部署,是构建可复现的前提。
一句话总结
docker 构建缓存陈旧的规律是「缓存键之外的变化不会触发重建」:改了代码仍 CACHED 先查 .dockerignore 与上下文路径,build-arg 只在引用它的层参与键,FROM 的基础镜像 digest 要用 --pull 刷新,多阶段误命中分段构建定位。排查的通式是找到第一个误命中的 CACHED 层,核对该层键的三要素——指令文本、父层 digest、COPY 校验和。治本靠「变化频率从低到高」的层序模板、CI 默认 --pull 与远程缓存配置、digest 钉死部署,--no-cache 只当止血不当疗法。