Dockerfile 写作精讲 / 10 - 缓存策略
10 - 缓存策略:缓存挂载、BuildKit 缓存与失效分析
10.1 缓存回顾
在第 01 章中我们介绍了层缓存的基本概念。本章将深入探讨 BuildKit 提供的高级缓存机制。
传统缓存 vs BuildKit 缓存
| 特性 | 传统缓存 | BuildKit 缓存 |
|---|---|---|
| 层级缓存 | ✅ | ✅ |
| 缓存挂载(cache mount) | ❌ | ✅ |
| 缓存导入/导出 | ❌ | ✅ |
| 后台并行构建 | ❌ | ✅ |
| 缓存垃圾回收 | 手动 | 自动 |
10.2 缓存挂载(Cache Mount)
--mount=type=cache 允许在构建过程中持久化缓存目录,跨构建共享。
包管理器缓存
# syntax=docker/dockerfile:1
# apt 缓存挂载
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt/lists \
apt-get update && apt-get install -y curl
# pip 缓存挂载
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# npm 缓存挂载
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Go module 缓存
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Go build 缓存
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o /server .
缓存挂载 vs 不挂载的对比
# ❌ 不使用缓存挂载:每次构建都重新下载
RUN pip install -r requirements.txt
# 耗时: 60s
# ✅ 使用缓存挂载:复用已下载的包
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# 首次: 60s 后续: 10s
缓存挂载的关键属性
RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache,sharing=locked \
pip install -r requirements.txt
| 属性 | 说明 | 默认值 |
|---|---|---|
target | 挂载到容器内的路径 | 必填 |
id | 缓存的唯一标识 | 自动基于 target |
sharing | 多构建并发时的共享策略 | shared |
mode | 目录权限 | 0755 |
uid / gid | 所有者 | 0 |
sharing 策略
| 策略 | 说明 |
|---|---|
shared | 多个构建可以同时读写同一个缓存 |
private | 每个构建使用独立的缓存副本 |
locked | 一次只有一个构建可以写入,其他等待 |
# 多阶段构建中共享缓存
FROM golang:1.22-alpine AS builder1
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
go mod download
FROM golang:1.22-alpine AS builder2
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
go mod download
10.3 缓存导入/导出(Cache Import/Export)
BuildKit 支持将构建缓存导出到外部存储,并在后续构建中导入。
本地缓存
# 导出缓存到本地目录
docker buildx build \
--cache-to type=local,dest=/tmp/buildcache \
-t myapp .
# 从本地目录导入缓存
docker buildx build \
--cache-from type=local,src=/tmp/buildcache \
-t myapp .
# 同时导入和导出
docker buildx build \
--cache-from type=local,src=/tmp/buildcache \
--cache-to type=local,dest=/tmp/buildcache,mode=max \
-t myapp .
Registry 缓存
# 导出缓存到 Registry
docker buildx build \
--cache-to type=registry,ref=registry.example.com/myapp:cache \
-t myapp .
# 从 Registry 导入缓存
docker buildx build \
--cache-from type=registry,ref=registry.example.com/myapp:cache \
-t myapp .
# 使用 inline 模式(缓存嵌入镜像层)
docker buildx build \
--cache-to type=inline \
-t myapp .
GitHub Actions 缓存
# .github/workflows/build.yml
name: Build
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: false
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
缓存模式对比
| 模式 | 导出内容 | 适用场景 |
|---|---|---|
min | 仅最终镜像层的缓存 | 大多数场景 |
max | 所有中间层的缓存 | 频繁修改中间层 |
# min 模式(默认)
docker buildx build --cache-to type=local,dest=cache,mode=min .
# max 模式(导出所有中间层)
docker buildx build --cache-to type=local,dest=cache,mode=max .
10.4 缓存失效的深度分析
层缓存失效链
FROM ubuntu:22.04 ← 如果 ubuntu:22.04 更新 → 后续全部失效
RUN apt-get update ... ← 指令变化 → 后续全部失效
COPY requirements.txt . ← 文件内容变化 → 后续全部失效
RUN pip install ... ← 指令变化 → 后续全部失效
COPY . . ← 任何文件变化 → 后续全部失效
RUN python setup.py ← 指令变化 → 后续全部失效
减少缓存失效的策略
策略一:将变化频率低的层放在前面
FROM python:3.12-slim
# 1. 系统依赖(几乎不变)
RUN apt-get update && apt-get install -y --no-install-recommends ...
# 2. Python 依赖(偶尔变化)
COPY requirements.txt .
RUN pip install -r requirements.txt
# 3. 应用代码(频繁变化)
COPY . .
策略二:分离不同变化频率的文件
FROM node:20-alpine
# 1. 包管理文件(偶尔变化)
COPY package.json package-lock.json ./
RUN npm ci
# 2. 配置文件(偶尔变化)
COPY tsconfig.json .eslintrc.json webpack.config.js ./
# 3. 源代码(频繁变化)
COPY src/ ./src/
# 4. 测试代码(仅测试阶段需要)
COPY test/ ./test/
策略三:使用 BuildKit 缓存挂载
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine
# 即使 go.mod 变化,已下载的模块仍被缓存
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download && go build -o /server .
10.5 缓存调试
查看构建过程中的缓存使用
# 使用 plain 输出查看每一步的缓存状态
docker build --progress=plain -t myapp .
# 输出中会显示 CACHED 或实际执行
# BuildKit 的详细输出
docker buildx build --progress=plain -t myapp .
# => CACHED [builder 3/5] RUN go mod download
# => DONE [builder 4/5] COPY . .
分析缓存失效原因
# 查看镜像层历史
docker history myapp:latest
# 使用 dive 工具分析层
dive myapp:latest
10.6 实战:完整的缓存优化方案
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /src
# 第一层:依赖(变化频率最低)
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# 第二层:源码(变化频率高)
COPY . .
# 第三层:编译(使用构建缓存)
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build \
-ldflags="-s -w" \
-o /server \
./cmd/server
# 生产阶段
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
ENTRYPOINT ["/server"]
# CI 构建命令(导入/导出缓存)
docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--platform linux/amd64,linux/arm64 \
-t myapp:latest \
--push .
10.7 常见错误与排查
| 错误 | 原因 | 解决方案 |
|---|---|---|
| 缓存从未命中 | 构建上下文每次都变化 | 优化 .dockerignore |
| 缓存挂载数据残留 | 缓存目录累积过期数据 | 定期清理或设置大小限制 |
| 并行构建冲突 | 多个构建同时写入缓存 | 使用 sharing=locked |
| CI 中缓存丢失 | 每次使用新 runner | 配置远程缓存(Registry/GHA) |
| 体积意外增大 | 使用 mode=max 导出过多缓存 | 改用 mode=min |
10.8 扩展阅读
上一章:09 - 多阶段构建 下一章:11 - BuildKit 高级特性 — 前端语法、Secrets、SSH 挂载与缓存导入导出。