在启用 BuildKit 后,Docker 构建通常会明显加快;但在 CI、多人协作或多架构构建中,也可能出现“明明没有修改依赖,却重新安装全部软件包”“相同提交重复生成镜像”“本地命中缓存,流水线却从零开始”等问题。🔍 排查时不要先急着执行全量清理,而应区分缓存键发生变化、缓存未被导入、缓存已被回收以及镜像被重复触发构建这几类情况。
一、先理解 BuildKit 的缓存判断方式
Dockerfile 中的指令会依次形成构建层。BuildKit 查找可复用结果时,不只是比较最终文件,还会综合基础镜像、指令内容、构建参数、挂载方式以及相关文件元数据生成缓存键。某一层失效后,后续依赖该层的步骤通常也需要重新执行,具体规则可参考 Docker 缓存失效说明。
对于 COPY、ADD 以及使用绑定挂载的 RUN 指令,BuildKit 会根据相关文件计算校验信息。文件内容、路径或部分元数据变化可能导致缓存失效,但仅修改文件的 mtime 通常不会触发 COPY 或 ADD 缓存失效。因此,不能只凭文件时间判断缓存是否应该命中。
二、从构建日志定位第一个失效步骤
排查的关键不是观察最后哪一步耗时最长,而是找到日志中第一个没有显示 CACHED 的步骤。建议使用纯文本进度输出,避免动态界面隐藏细节:
docker buildx build --progress=plain -t myapp:test .
将本次日志与上一次正常命中缓存的日志对比,重点检查步骤编号、构建上下文大小、基础镜像摘要、Dockerfile 指令和平台参数。如果某个 COPY 首先失效,应继续检查它复制了哪些文件;如果 FROM 首先变化,则应核对基础镜像标签或摘要。
建议记录的最小排查信息
- Docker、Buildx 与 BuildKit 的版本,以及当前使用的 builder 名称和驱动类型。
- 完整构建命令,包括 --platform、--build-arg、--target、--cache-from 和 --cache-to。
- 首次出现非 CACHED 状态的步骤及其前一层。
- 本地、CI 和其他构建节点是否读取同一个缓存来源。
- 同一提交是否被多个流水线事件、矩阵任务或重试机制重复触发。
三、检查构建上下文与 .dockerignore
最常见的缓存破坏方式,是过早执行 COPY . .。只要上下文中被复制的源码、日志、测试报告或临时文件发生变化,这一层及其后续步骤就可能重新构建。📦 应使用 .dockerignore 排除 .git、node_modules、构建产物、编辑器缓存、覆盖率报告和本地日志,但不要误排除构建真正需要的文件。
依赖文件应与业务源码分开复制。例如先复制 package-lock.json、poetry.lock、go.mod 或 pom.xml,完成依赖安装后,再复制经常变化的源码。这样修改业务代码时,依赖安装层仍有机会命中缓存。原则是:稳定步骤靠前,频繁变化的内容靠后。
四、核对基础镜像、参数和环境差异
使用 latest、滚动标签或未固定摘要的基础镜像时,远端标签可能指向新的镜像内容,从 FROM 开始改变整个缓存链。需要稳定复现时,可以选择明确版本,并在供应链策略允许的情况下固定镜像摘要;同时仍应制定升级周期,避免为了缓存长期停留在存在风险的旧镜像上。
ARG 值、目标平台、构建阶段和 Dockerfile 前端版本也会影响结果。特别要检查 CI 是否把提交号、构建时间或流水线编号作为 ARG 传入了前置阶段。若动态值在依赖安装之前参与构建,几乎每次运行都会生成新的缓存键。可将版本标签写入靠后的独立步骤,减少对前面稳定层的影响。
如果设置了 SOURCE_DATE_EPOCH,还要确认它是否每次构建都变化。Docker 文档指出,该参数变化可能影响 WORKDIR 及后续指令的缓存有效性。用于可复现构建时,应明确选择固定值还是随提交变化的值,避免两种目标相互冲突。
五、确认 CI 中的缓存确实被保存和恢复
本地缓存通常存在于当前 builder 的存储中,而 CI Runner 可能是临时环境,任务结束后缓存随之消失。此时即使 Dockerfile 完全相同,下一次任务也没有可读取的历史结果。应为流水线配置对应的缓存后端,并同时设置导入与导出,例如 registry、local、GitHub Actions 或其他受支持后端,具体方式可查看 缓存存储后端文档。
配置 registry 缓存时,需要检查仓库地址、标签、登录权限以及任务结束前是否成功推送缓存。使用 local 缓存时,则要确认目录已被 CI 缓存机制持久化,并在后续任务恢复到相同路径。多分支项目还应设计合理的缓存作用域:过度隔离会降低命中率,所有分支共用一个可变引用则可能互相覆盖。
还要确认不同任务是否使用了不同 builder。执行以下命令可查看当前构建器及其节点状态:
docker buildx ls
docker buildx inspect --bootstrap
如果本地使用 docker 驱动,而 CI 使用 docker-container 或远程 BuildKit,二者的缓存存储位置并不相同。不要把“镜像仓库里已有同名镜像”等同于“当前 builder 拥有可用构建缓存”;只有正确导入包含缓存元数据的来源,BuildKit 才能复用对应结果。
六、区分缓存失效与镜像重复构建
镜像重复出现不一定是缓存问题,还可能是流水线重复执行。🚦 检查 push 与 pull_request 是否同时触发、合并提交是否再次触发、矩阵任务是否都在构建相同平台,以及失败重试是否创建了新任务。可以在日志中输出提交 SHA、事件类型、目标平台、镜像标签和流水线运行编号,以判断多个镜像是否来自同一次代码变更。
多架构构建也容易造成误判。amd64 与 arm64 通常需要各自执行平台相关步骤,部分缓存不能跨平台直接复用。最终推送的多架构清单可能引用多个平台镜像,这属于正常结果,不应简单视为重复镜像。应比较平台、配置摘要和清单关系,而不是只看仓库中的条目数量。
七、谨慎使用无缓存构建与清理命令
--no-cache 适合验证问题是否由旧缓存引起,但不应作为日常修复方案;它会主动放弃可复用结果,却不能解决缓存未持久化、构建参数漂移或流水线重复触发。需要更新基础镜像时,可结合实际需求使用 --pull,而不是每次都执行全量无缓存构建。
执行 docker builder prune 或 docker buildx prune 前,应先使用 docker system df、docker buildx du 等命令了解空间占用和可回收对象。🧹 盲目清理会删除原本有效的缓存,使下一次构建变慢,并掩盖真正的失效原因。共享构建节点还应配置容量与回收策略,避免缓存因磁盘压力被不可预期地淘汰。
八、推荐的排查顺序
- 使用 --progress=plain 保存完整日志,找到第一个未命中缓存的步骤。
- 核对基础镜像摘要、平台、target、ARG 和 Dockerfile 是否一致。
- 检查 COPY 范围、构建上下文内容以及 .dockerignore 是否合理。
- 确认当前 builder、驱动和节点没有在任务之间发生变化。
- 验证 cache-from 是否成功导入、cache-to 是否完成导出。
- 检查缓存是否因容量限制、垃圾回收或临时 Runner 销毁而消失。
- 核对 CI 触发器、矩阵任务和重试记录,排除同一提交被重复构建。
- 调整 Dockerfile 层次后,用连续两次相同构建验证 CACHED 状态。
总结
BuildKit 缓存问题应沿着“首次失效层—缓存键输入—缓存存储位置—流水线触发来源”逐层分析。✅ 优先缩小 COPY 范围、把稳定步骤前置、避免动态参数污染前置层,并为临时 CI 环境配置可持久化的缓存后端。验证修复时,应连续执行相同构建并比较日志,而不是仅凭总耗时下结论。这样既能减少无意义的重复构建,也能在保证镜像可复现和依赖可更新的前提下,提高流水线效率。