
Docker BuildKit 缓存失效的 5 个真实场景:GitHub Actions CI/CD 提速避坑指南
明明什么都没改,GitHub Actions 的 Docker 镜像构建每次都从零开始。层缓存失效是 CI 慢的根因,但 BuildKit 在 GitHub Actions 环境里的失效模式和本地完全不同——认证 token 过期、per-step tmpfs、多平台缓存隔离、系统时间戳、.dockerignore 遗漏,每个都是独立坑。
本文覆盖 5 个我实际踩过的场景,每个都有可复现的命令和验证方法。
问题场景一:cache-from 用了私有镜像,认证 token 过期导致全量回退
场景一:私有镜像的 cache-from 认证 token 过期。GitHub Packages 或其他私有仓库需要认证才能拉取镜像层,cache-from 配置里用了需要 token 的镜像地址时,如果 token 失效,BuildKit 会静默退化为无缓存构建——CI 日志里完全看不到报错。
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build with cache
uses: docker/build-push-action@v5
with:
cache-from: type=registry,ref=ghcr.io/your-org/your-image:latest
cache-to: type=registry,mode=max,ref=ghcr.io/your-org/your-image:latest
push: true
问题在于 GITHUB_TOKEN 的有效期是 24 小时(可配置,最长 90 天,但需要额外申请)。超过有效期后,docker/build-push-action 会静默退化为无缓存构建,因为拉取 cache-from 镜像层时认证失败,BuildKit 选择跳过缓存而不是报错。
验证方法——在 CI 日志里搜索 cache not found 或 authentication required:
# 本地模拟验证(用已过期的 token 测试)
docker buildx build \
--cache-from ghcr.io/your-org/your-image:latest \
--push false 2>&1 | grep -i "cache\|auth"
# 输出如果包含 "cache not found" 或 "retrying" 说明缓存拉取失败
解法:改用 GitHub OIDC 认证,或在 workflow 里每天重新登录:
# 使用 OIDC 方式(推荐)
- name: Login to GHCR via OIDC
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 如果 token 过期严重,改用_PAT 或配置 token 有效期
问题场景二:GitHub Actions 的临时文件系统导致 cache 目录权限变更
GitHub Actions 的 runner 在每次 job 开始时会创建一个新的临时文件系统(overlay fs)。如果你在同一个 job 里先 docker build 再做其他操作,然后第二次构建,BuildKit 的缓存目录(默认在 /root/.buildcache 或 /root/.cache/buildkit)可能会因为 runner 环境的临时性被部分清理。
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build first time
run: |
docker buildx create --use
docker buildx build \
--cache-from type=local,src=/tmp/docker-cache \
--cache-to type=local,dest=/tmp/docker-cache-new \
--tag myapp:${{ github.sha }} .
- name: Build second time (uses cache)
run: |
# 注意:/tmp 在 GitHub Actions 里是 per-step 的,
# 上一步的 /tmp/docker-cache-new 在这一步不可见!
docker buildx build \
--cache-from type=local,src=/tmp/docker-cache \
--tag myapp:${{ github.sha }} .
GitHub Actions 的 /tmp 是 per-step 的,不是 per-job 的。所以 cache-from type=local,src=/tmp/docker-cache 在不同 step 之间完全不共享。
验证方法——在两个 step 里分别检查缓存目录:
- name: Check cache in step 1
run: |
echo "Step 1 tmp: $(ls -la /tmp)"
docker buildx build --cache-from type=local,src=/tmp/my-cache ...
- name: Check cache in step 2
run: |
echo "Step 2 tmp: $(ls -la /tmp)" # 会看到不同的 /tmp 内容
# /tmp/my-cache 在这里不存在了
解法:使用 GitHub Actions 提供的 actions/cache action 来持久化缓存目录,或直接用 registry 缓存:
- name: Build and push with registry cache
uses: docker/build-push-action@v5
with:
cache-from: type=registry,ref=ghcr.io/your-org/your-image:buildcache
cache-to: type=registry,ref=ghcr.io/your-org/your-image:buildcache,mode=max
tags: ghcr.io/your-org/your-image:${{ github.sha }}
push: true
问题场景三:多平台构建导致缓存完全不共享
如果你在 CI 里同时构建 linux/amd64 和 linux/arm64 两个平台,BuildKit 的 layer 缓存是严格平台相关的。amd64 的缓存对 arm64 完全无效,反之亦然。
# 这个配置会产生"明明用了缓存但还是很慢"的问题
- name: Build multi-platform
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
cache-from: ghcr.io/your-org/your-image:buildcache
cache-to: ghcr.io/your-org/your-image:buildcache,mode=max
push: true
原因:不同 CPU 架构的二进制文件不同,BuildKit 对每个平台独立计算 layer hash。arm64 的 RUN apt-get install -y python3 产生的 layer 和 amd64 的完全不同。
验证方法——检查 BuildKit 的缓存分析输出:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from ghcr.io/your-org/your-image:buildcache \
--push false \
2>&1 | grep -E "CACHED|layer|platform"
# 你会看到 amd64 和 arm64 分别处理,但彼此的缓存不共享
正确的做法是每个平台用独立的 cache tag:
# 分别构建,避免缓存污染和跨平台失效
- name: Build amd64
uses: docker/build-push-action@v5
with:
platform: linux/amd64
cache-from: type=registry,ref=ghcr.io/your-org/your-image:buildcache-amd64
cache-to: type=registry,ref=ghcr.io/your-org/your-image:buildcache-amd64,mode=max
tags: ghcr.io/your-org/your-image:latest-amd64
push: true
- name: Build arm64
uses: docker/build-push-action@v5
with:
platform: linux/arm64
cache-from: type=registry,ref=ghcr.io/your-org/your-image:buildcache-arm64
cache-to: type=registry,ref=ghcr.io/your-org/your-image:buildcache-arm64,mode=max
tags: ghcr.io/your-org/your-image:latest-arm64
push: true
问题场景四:Dockerfile 里 system time 不一致导致 layer hash 全量失效
这是我在实际项目里踩过的坑。当 GitHub Actions runner 的系统时间发生微小变化(例如由于 NTP 校准),Dockerfile 里的 RUN 指令即使内容完全相同,每次构建产生的 layer hash 也会不同,导致缓存完全失效。
# 每次 CI 运行时间不同,导致这行 RUN 的时间戳变化
RUN date && pip install -r requirements.txt
# 正确做法:不包含 date 或使用固定时间
RUN pip install -r requirements.txt
还有一个不易察觉的坑:某些工具会在 RUN 时写入当前时间导致 layer hash 变化:
# 这些指令每次运行都会改变 layer hash
RUN echo "Built at $(date)" > /app/version.txt
RUN npm version from-git # npm 会写入当前 git tag 作为版本
RUN python -c "import datetime; print(datetime.datetime.now())"
# 正确做法:将动态内容移到运行时
CMD ["python", "-c", "import datetime; print(datetime.datetime.now())"]
验证方法——连续两次构建,观察 layer 数量:
# 第一次构建
docker buildx build --no-cache --push false -t test:1 .
# 观察输出中的 "CACHED" 行数量
# 第二次完全相同的构建
docker buildx build --no-cache --push false -t test:2 .
# 对比两次构建的 layer 数量和 hash
docker buildx imagetools inspect test:1 | jq '.manifests[0].digest'
docker buildx imagetools inspect test:2 | jq '.manifests[0].digest'
# 如果 hash 不同,说明有动态内容污染了缓存
解法:使用 BuildKit 的 BUILDKIT_INLINE_CACHE 和排除时间相关的文件:
# 在 .dockerignore 里排除
echo "version.txt" >> .dockerignore
echo ".npmrc" >> .dockerignore
# 或者在 BuildKit 配置里强制稳定 layer hash
docker buildx build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from ghcr.io/your-org/your-image:buildcache \
--push true
问题场景五:依赖层的 .dockerignore 配置错误导致缓存污染
.dockerignore 的作用不仅是为了减小构建上下文大小,它还直接影响 BuildKit 的缓存判断。当 .dockerignore 漏掉了不应该进入镜像的文件,但这些文件的变化又没有被正确感知时,缓存会悄悄失效。
# 错误的 .dockerignore —— 漏掉了会触发依赖重新安装的文件
# 实际项目里可能漏掉:
# - .env(包含依赖版本)
# - package-lock.json(npm)
# - requirements.lock(pip)
# 正确的做法:明确列出要排除的,而不是要包含的
*
!src
!requirements.txt
!pyproject.toml
!Dockerfile
# 排除所有本地开发文件
.env
.git
__pycache__
*.pyc
.venv
node_modules
实战案例:项目里 requirements.txt 没变,但 .dockerignore 漏掉了 .venv/。本地开发者在 .venv 里多装了包,这个变化会被计入构建上下文哈希,导致 BuildKit 认为源码变了,强制重装依赖。
验证方法——用 docker buildx build --progress=plain 观察缓存命中情况:
docker buildx build \
--cache-from ghcr.io/your-org/your-image:buildcache \
--progress=plain \
--push false 2>&1 | grep -E "CACHED|#\d+"
# CACHED 表示命中,#1 #2 表示新增层
# 如果每次 #3(COPY requirements.txt)都重新执行,说明上下文检测有问题
现在你可以做什么
第一步:检查你的 CI 日志里有没有 cache not found 或 retrying 的字样,有的话先解决认证问题。
第二步:如果你在用 type=local 的 cache-from,确认 GitHub Actions job 内缓存持久化用的是 actions/cache 而不是直接挂载 /tmp。
第三步:多平台构建场景下,给每个平台分配独立的 cache tag,避免跨平台污染。
第四步:搜索你的 Dockerfile 里有没有 $(date)、npm version、git describe 等会在构建时产生变化的命令,把它们移到运行时执行。
第五步:审查 .dockerignore,确保它排除了所有不需要进入镜像的文件,尤其是 .venv/、__pycache__/、node_modules/ 这些大目录。
更多交流点击入群






