强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 挂载与缓存导入导出。