Docker BuildKit 缓存失效:本地能跑 CI 不行的 2 个真实场景

封面图

# Docker BuildKit 缓存失效:本地能跑 CI 不行的 2 个真实场景

本地开发好好的 Dockerfile,CI 里每次都从零编译。一个下午排查完才发现——问题不在 Dockerfile,在于 CI 环境和本地的三处差异。

问题现象:什么都没改,CI 却每次从零编译

在 GitHub Actions 里跑 Docker 构建,发现即使代码没动,缓存命中率也是 0%。本地 docker build 却次次命中,构建只需 30 秒,CI 却要 5 分钟。

这不是运气问题,是 CI 环境有 3 个本地不会遇到的缓存失效机制:认证 token 过期构建上下文差异平台架构不同。本文重点讲前两个。

场景一:GITHUB_TOKEN 过期导致 cache-from 认证失败

Dockerfile 用 cache-from 复用其他镜像的层:

``dockerfile
# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS builder
RUN --mount=type=cache,target=/root/.npm \
npm install
`

CI 里加了 --cache-from ghcr.io/myorg/myimage:build,本地正常,CI 每次都从零跑 npm install

根因:GitHub Actions 的 GITHUB_TOKEN 有效期 24 小时。私有镜像 registry(如 GHCR)的 docker login 依赖这个 token。过期后 cache-from 能拉镜像但无法拉缓存层(认证失败不报错,缓存静默失效)。

验证方法

`bash
# 手动触发重新登录(续命 token)
docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< "${{ secrets.GITHUB_TOKEN }}" # 或者加到 workflow 每次构建前

  • name: Login to GHCR

uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
`

加了 login-action 之后,缓存命中率从 0% 恢复到 70%+。

为什么本地没问题:本地 docker login 是永久的(除非手动 docker logout),CI 的 token 每次 workflow 触发时刷新,但 login action 不会自动执行。

场景二:.dockerignore 漏掉大目录,上下文哈希每次变化

项目根目录有 .venv/node_modules/,Docker 构建上传上下文时,这些目录每次都打包上传。即使 .dockerignore 写了也无效——因为 BuildKit 的 cache-from 缓存 Key 不只是 Dockerfile 的 hash,还包括构建上下文的 hash。

实测数据:

| 操作 | 上下文大小 | 缓存命中率 |
|------|-----------|-----------|
| 未配置 .dockerignore | 420 MB | 0%(每次重传) |
| 加 .dockerignore 后首次 | 12 MB | 100% |
| 加了 .venv 后再次构建 | 420 MB | 0% |

解法.dockerignore 要明确包含而不是排除关键目录:

`dockerignore
# 明确包含需要的文件,排除其他一切
*
!src
!package.json
!requirements.txt
!Dockerfile
.venv
node_modules
.git
.github
`

* 排除所有,再 ! 显式恢复需要的目录。这样即使有人在根目录放了 temp/, logs/, .cache/ 也不会污染上下文。

为什么本地没问题:本地构建 docker build . 时,上下文就是当前目录。如果 .dockerignore 正常,本地缓存本来就有效。CI 的 runner 每次都是全新环境,.dockerignore 写漏了就会暴露这个问题。

排查清单:快速定位是哪类缓存失效

遇到 CI 缓存失效,先用这个顺序排查:

`bash
# 1. 确认缓存是否真的被尝试
docker buildx build \
--cache-from type=registry,ref=ghcr.io/myorg/myimage:build \
--progress=plain 2>&1 | grep -i "cache"

# 2. 验证认证状态(看是否有 CACHED)
docker buildx build \
--cache-from type=registry,ref=ghcr.io/myorg/myimage:build \
--progress=plain 2>&1 | grep -E "CACHED|#\d+"

# 3. 检查构建上下文大小
docker buildx build \
--progress=plain \
--build-arg BUILDKIT_CONTEXT_KEEP_HISTORY=1 \
-t myorg/myimage:test . 2>&1 | grep "uploaded"
`

如果看到 uploaded 420 MB in 30s 这样的输出,说明上下文每次都全量上传,是 .dockerignore 的问题。

如果看到 trying cache-puller 但没有 CACHED,是认证问题。

现在你可以做什么

第一步:检查你的 .dockerignore 是否用 * 开头排除所有,再显式 ! 恢复关键目录。如果用的是排除式写法(node_modules/__pycache__/),改成包含式。

第二步:如果用了 cache-from 拉私有镜像,在 GitHub Actions workflow 开头加上 docker/login-action@v3

`yaml

  • uses: docker/login-action@v3

with:
registry: ghcr.io # 或你的私有 registry
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
`

第三步:在 CI 日志里搜索 uploaded,确认上下文大小。超过 100MB 的构建大概率需要优化 .dockerignore

第四步:如果多平台构建(arm64 + amd64),给不同平台用不同的 cache tag(如 :build-arm64:build-amd64`),避免交叉污染。

© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享