用 BuildKit 缓存挂载把镜像构建从 8 分钟压到 40 秒
用 BuildKit 缓存挂载把镜像构建从 8 分钟压到 40 秒
一、问题:每次改一行代码,等 8 分钟
典型的多阶段 Dockerfile 往往长这样:
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:1.27
COPY --from=build /app/dist /usr/share/nginx/html
这段写法在 CI 上是灾难:COPY . . 放在 npm ci 之前,意味着只要改了一个源文件,整个 npm ci 的层缓存就失效,依赖要重新下载一遍。依赖 400 多个包的项目,光这一步就要几分钟。
常规优化是调整 COPY 顺序、把 package.json 与 package-lock.json 单独先拷进来。这确实有效,但只要锁文件有一点点变化,仍然要全量重装。真正一劳永逸的办法是 BuildKit 的缓存挂载(cache mount):把包管理器的缓存目录挂到构建器上,跨构建复用。
二、开启 BuildKit
Docker 23 之后 BuildKit 已经是默认构建器,可以直接确认:
docker buildx version
docker buildx ls
如果还在用旧版本,设置环境变量或在 daemon.json 里打开:
{
"features": { "buildkit": true }
}
三、给 npm 加缓存挂载
改造后的 Dockerfile:
# syntax=docker/dockerfile:1.7
FROM node:20 AS build
WORKDIR /app
# 只拷贝依赖清单,先装依赖
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline --no-audit
# 再拷贝源码
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
关键就是这一行:
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline --no-audit
--mount=type=cache 让 /root/.npm 在多次构建之间保留下来。npm ci 依然会按锁文件精确安装,但 tarball 都命中本地缓存,网络请求几乎为零。--prefer-offline 进一步减少元数据查询。
同样的思路适用于其他语言:
- Python / pip:
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt - Go:
RUN --mount=type=cache,target=/go/pkg/mod go mod download - Rust / cargo:
RUN --mount=type=cache,target=/usr/local/cargo/registry cargo build --release - apt:
RUN --mount=type=cache,target=/var/cache/apt apt-get update && apt-get install -y curl
四、注意:缓存不是镜像层
这是最容易踩的坑。cache mount 的内容 不会进入最终镜像,也不会被推到 registry,它只存在于构建器本地(buildx 的 builder 实例里)。
所以要区分两件事:
- 想加速构建 → 用 cache mount
- 想固化产物 → 用普通
RUN或COPY --from
比如 Go 项目里,用 cache mount 装依赖,但编译产物要显式拷到最终镜像:
FROM golang:1.23 AS build
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 -o /out/app ./cmd/app
FROM gcr.io/distroless/static
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
/go/pkg/mod 加速依赖下载,/root/.cache/go-build 加速编译本身,/out/app 才是真正进镜像的产物。
五、CI 里怎么让缓存真正生效
本地构建还好,CI 上每次都是干净环境,需要显式共享缓存:
方案一:registry 缓存(最省事)
docker buildx build \
--cache-from type=registry,ref=registry.example.com/app:buildcache \
--cache-to type=registry,ref=registry.example.com/app:buildcache,mode=max \
-t registry.example.com/app:latest --push .
方案二:GitHub Actions 的本地缓存
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: registry.example.com/app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
mode=max 会缓存中间层,代价是缓存体积变大;如果镜像层次少,用默认 min 就够。
六、实测数据
一个中等规模的 Node 前端项目,同一台 8C16G 的构建机:
| 场景 | 耗时 |
|---|---|
| 原始 Dockerfile,改一行源码 | 8 分 10 秒 |
| 只调整 COPY 顺序 | 3 分 20 秒 |
| 调整 COPY + npm cache mount | 55 秒 |
| 上面再加 registry 缓存复用 | 40 秒 |
另外 --mount=type=cache 还支持 sharing=locked,在同一个缓存目录被多个并发构建写入时(比如 CI 并行跑两个分支)建议加上,避免缓存互相踩踏:
RUN --mount=type=cache,target=/root/.npm,sharing=locked npm ci
七、小结
BuildKit 的 cache mount 是这几年来 Docker 构建侧性价比最高的一个改动:不需要改项目结构,不需要引入额外的缓存服务,几行 Dockerfile 就能把重复下载和重复编译省掉。真正要记住的两点:
- 缓存目录按语言选对(npm →
/root/.npm,pip →/root/.cache/pip,go →/go/pkg/mod); - 缓存不会进镜像,需要固化到镜像里的东西必须显式 COPY 出来。
把这两点写进团队的基础镜像模板,后续新项目直接受益。
- 点赞
- 收藏
- 关注作者
评论(0)