Docker 层缓存实战:把 CI 构建时间从 5 分钟压缩到 30 秒

主题: docker-layer-caching-ci-build-speedup更新于: 2026/6/25作者:AgentFactory 技术团队

如果你的 CI 流水线每次代码推送都要重新下载所有依赖、编译整个项目,那 5 分钟的构建时间就是纯粹的浪费。本文不讲理论,直接给出可落地的 Docker 层缓存方案,重点解决「缓存不生效」「缓存体积爆炸」「多阶段构建缓存失效」这三个核心痛点。

它解决什么问题 / 适用场景

任何使用 Docker 构建的 CI/CD 流水线,只要满足以下条件,这套方案都能显著提速:

  • 构建时间超过 2 分钟:每次构建都要下载依赖(npm install、pip install、go mod download),这些操作完全可以缓存
  • 频繁构建:每次代码推送、PR 合并都触发构建,一天几十次
  • 使用临时运行器:GitHub Actions、GitLab CI、CircleCI 等每次分配全新环境,没有本地缓存
  • 项目依赖体积大:Node.js 的 node_modules、Python 的 pip 包、Go 的 mod 缓存,这些是缓存优化的主战场

核心配置 / 参数说明

BuildKit 提供了三种缓存后端,通过 --cache-from--cache-to 参数控制。以下是完整参数说明:

参数是否必须说明
--cache-from指定缓存来源。可选值:type=gha(GitHub Actions 缓存)、type=registry,ref=<image>(注册表缓存)、type=local,src=<path>(本地目录缓存)
--cache-to指定缓存目标。可选值同上,但 type=local 使用 dest= 而非 src=。通常与 mode=max 配合使用
mode--cache-to 的子参数。max 缓存所有阶段的所有层(推荐 CI 使用),min(默认)只缓存最终阶段的层

关键选择建议

  • GitHub Actions 用户:优先使用 type=gha,配置最简单,缓存命中率高
  • 自托管运行器:使用 type=local,配合持久化存储
  • 跨平台/跨项目共享缓存:使用 type=registry,将缓存推送到镜像仓库

安装与快速上手

前置条件

  • Docker 19.03+(内置 BuildKit)
  • 启用 BuildKit:设置环境变量 DOCKER_BUILDKIT=1,或使用 docker buildx build 命令

最小化配置示例(GitHub Actions)

YAML
# .github/workflows/docker-build.yml
name: Docker Build with Cache

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and cache
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: myregistry.com/myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

注意:首次构建时,缓存源(type=gha)尚不存在,会报错 failed to fetch cache from remote: not found。解决方案见下文「常见报错与排查」。

注册表缓存方案(适用于任何 CI)

BASH
# 构建并推送缓存到镜像仓库
docker buildx build \
  --cache-from type=registry,ref=myregistry.com/myapp:cache \
  --cache-to type=registry,ref=myregistry.com/myapp:cache,mode=max \
  -t myregistry.com/myapp:latest \
  --push \
  .

与同类方案对比

对比维度type=gha(本文推荐)type=registrytype=local
缓存机制层缓存,使用 GitHub Actions 缓存 API层缓存,存储在镜像仓库层缓存,存储在本地目录
持久化方式CI 原生缓存(GitHub Actions)镜像仓库自托管运行器的持久化存储
多阶段构建支持完全支持(配合 mode=max完全支持(配合 mode=max完全支持(配合 mode=max
配置复杂度低(一行 cache-from + cache-to中(需要管理缓存镜像标签)低(但需要持久化目录)
跨分支/跨项目共享仅限同一仓库支持(通过镜像仓库)仅限同一运行器
首次构建速度无缓存,正常速度无缓存,正常速度无缓存,正常速度
缓存命中率高(基于缓存键匹配)高(基于镜像层哈希)高(基于层哈希)

亮点type=gha 是 BuildKit 与 GitHub Actions 深度集成的现代方案,无需额外配置缓存键,自动基于 Dockerfile 内容和构建上下文生成缓存键,命中率极高。

生产环境实践与注意事项

1. 缓存失效的不可预测性

即使依赖文件未变,某些 CI 平台(如 GitHub Actions)的缓存键可能因运行器环境变化(如操作系统更新、Docker 版本升级)而失效,导致缓存完全丢失。解决方案:固定基础镜像标签(如 node:20-alpine 而非 node:20),避免使用 latest 标签。

2. 缓存存储成本

使用 mode=max 会缓存所有层,包括中间阶段,这可能导致缓存体积迅速膨胀(可达数 GB),增加存储费用(如 GitHub Actions 的缓存限制为 10GB/仓库)。解决方案

  • 先使用 mode=max 评估缓存命中率,如果缓存体积过大,降级为 mode=min
  • .dockerignore 中排除不必要的文件(如 .gitnode_modules
  • 定期清理旧的缓存条目:gh cache delete --repo owner/repo

3. 安全风险

缓存中可能包含敏感信息(如环境变量、私钥),如果缓存被其他分支或外部构建访问,可能导致信息泄露。解决方案:使用 --secret--ssh 传递敏感数据,而非写入层。

4. 并发冲突

当多个 CI 作业同时写入同一个缓存目标(如注册表缓存 type=registry)时,可能发生竞态条件,导致缓存损坏或构建失败。解决方案:使用 type=gha(GitHub Actions 原生支持并发写入),或为每个分支使用独立的缓存标签。

5. 网络依赖

注册表缓存(type=registry)和 GitHub Actions 缓存(type=gha)都依赖网络,网络抖动或超时可能导致构建失败。解决方案:在 CI 脚本中添加重试逻辑,或设置超时时间。

6. 本地目录缓存限制

type=local 仅适用于具有持久化存储的自托管运行器,在临时运行器上无效。

常见报错与排查

ERROR: failed to solve: failed to fetch cache from remote: not found

原因:首次构建时,缓存源(如 type=ghatype=registry)尚不存在。

解决方案:在 CI 脚本中添加条件判断,如果缓存不存在则忽略 --cache-from 参数:

BASH
# 首次构建时忽略缓存源
docker buildx build \
  --cache-from type=gha \
  --cache-to type=gha,mode=max \
  -t myregistry.com/myapp:latest \
  . || docker buildx build \
  --cache-to type=gha,mode=max \
  -t myregistry.com/myapp:latest \
  .

ERROR: cache export: failed to write cache: max cache size exceeded

原因:缓存大小超过了 CI 平台或注册表的限制。

解决方案

  1. 使用 mode=min 代替 mode=max,只缓存最终阶段的层
  2. .dockerignore 中排除不必要的文件(如 .gitnode_modules
  3. 对于 GitHub Actions,清理旧的缓存条目:gh cache delete --repo owner/repo

ERROR: cache import after build: failed to get cache: no such file or directory

原因:使用 type=local 缓存时,指定的本地目录不存在。

解决方案:在构建前创建该目录:

BASH
mkdir -p /tmp/docker-cache
docker buildx build \
  --cache-from type=local,src=/tmp/docker-cache \
  --cache-to type=local,dest=/tmp/docker-cache,mode=max \
  -t myregistry.com/myapp:latest \
  .

WARNING: cache is not shared between builds due to different cache keys

原因:缓存键不匹配导致缓存未命中。

常见原因

  1. 构建上下文(context)路径不同
  2. Dockerfile 内容或基础镜像标签(如 node:20-alpine vs node:20)不同
  3. 使用了动态标签(如 git rev-parse --short HEAD)作为缓存源

解决方案

  • 固定基础镜像标签,避免使用 latest
  • 使用稳定的缓存键(如分支名或 latest 标签)
  • 确保所有构建使用相同的上下文路径

常见问题 FAQ

Q: 为什么我按照文章优化了 Dockerfile 顺序,但 CI 构建时间并没有明显减少?

A: 最常见的原因是 CI 运行器是临时的,没有持久化的 Docker 缓存。文章中提到,即使 Dockerfile 顺序正确,如果每次构建都是全新的环境,缓存仍然为空。你需要实施缓存持久化策略,例如使用 BuildKit 的 type=gha(GitHub Actions)或 type=registry(注册表)缓存后端。另外,检查 .dockerignore 文件是否遗漏了 .git 目录,这会导致每次 COPY 层都因 .git 目录变化而失效。

Q: 使用 mode=max 缓存所有层,会不会导致缓存体积过大,影响构建速度?

A: 是的,mode=max 会缓存所有中间阶段和最终阶段的层,缓存体积可能迅速增长到数 GB。这可能导致两个问题:1) 缓存上传/下载时间增加,抵消了缓存带来的速度提升;2) 超过 CI 平台的缓存存储限制(如 GitHub Actions 的 10GB/仓库)。建议:对于大型项目,先使用 mode=max 评估缓存命中率,如果缓存体积过大,可降级为 mode=min(只缓存最终阶段),或结合 .dockerignore 精简构建上下文。另外,定期清理旧的缓存条目(如使用 gh cache delete 命令)也是必要的。

Q: 多阶段构建中,如何确保依赖安装阶段(如 deps)的缓存被正确保存和恢复?

A: 关键在于使用 BuildKit 的 --cache-to--cache-from 参数,并设置 mode=maxmode=max 会缓存所有阶段的所有层,包括 deps 阶段。在 CI 脚本中,确保每次构建都使用相同的缓存源(如 type=ghatype=registry)。另外,注意多阶段构建中的 COPY --from 指令:如果源阶段(如 deps)的层被缓存命中,COPY --from 操作会直接从缓存中复制文件,速度极快。如果缓存未命中,则会重新构建源阶段。因此,保持依赖文件(如 package.json)的稳定是最大化缓存命中率的关键。

相关深度解决方案

在配置当前服务时,如果您遇到了数据库锁死或需要更高并发的读写控制,建议配合参考我们整理的 SQLite MCP 服务的高级缓存配置指南 来提升响应速度。