BuildKit 是替代 Docker 旧版 legacy builder 的现代镜像构建引擎。如果你多年来一直在用 docker build 而没有回头看,你错过了三样东西:跨构建保留包管理器下载的 cache mount、让多阶段构建更快完成的并行阶段执行,以及为 linux/amd64 和 linux/arm64 生成单一 manifest 的原生多架构构建支持。我用 docker buildx 构建了一个小型 Go 服务,在每一步都捕获了真实输出,并实测了时间花在了哪里。以下全部内容复制自终端——没有理论,没有文档截图。
BuildKit 是什么,为什么更快
旧版 builder 逐条顺序执行 Dockerfile 指令。BuildKit(docker buildx 背后的引擎)构建依赖图并并行运行独立阶段。对于像”用 Go 编译,然后把二进制文件复制到精简 runtime 镜像”这样的多阶段构建,runtime 阶段的 apt-get 可以在编译器还在预热时就开始。在此基础上,BuildKit 引入了 cache mount——挂载到单条 RUN 指令的持久化卷——这样 apt-get download、go mod download 和 npm ci 就能跨构建保留缓存,而不是把它们烤进层里。
实际结果:我的 Go 镜像在完全没有层缓存的情况下冷构建大约需要 90 秒。同样的镜像在 BuildKit 缓存预热后重构建只花了 3.5 秒。这不是渐进式改进——这是完全不同的工作流。
Cache mount:改变一切的特性
Cache mount 是 BuildKit 管理的持久化卷,挂载到一条 RUN 指令上。它不是层——永远不会出现在最终镜像中。它存在的意义纯粹是让 apt、pip、go 和 npm 跨构建保留下载缓存。以下是我对 apt 使用的模式:
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git && rm -rf /var/lib/apt/lists/*
第一次运行时,apt 从网络下载 9.4 MB 包。第二次——即使是在完全不同的镜像上,即使执行过 docker system prune——包直接从 cache mount 读取。以下是证据。第一次构建,apt 从网络拉取:
#12 [builder 3/7] RUN --mount=type=cache,target=/var/cache/apt,sharing=locked ...
#12 1.636 Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
#12 1.912 Get:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
#12 2.009 Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [34.8 kB]
#12 2.102 Get:4 http://deb.debian.org/debian bookworm/main amd64 Packages [8790 kB]
#12 8.508 debconf: delaying package configuration, since apt-utils is not installed
#12 9.160 Unpacking ca-certificates (20250419~deb12u1) ...
#12 DONE 32.0s
同样的命令,第二次构建——全部 CACHED,apt 完全不碰网络:
#14 [builder 3/7] RUN --mount=type=cache,target=/var/cache/apt,sharing=locked ...
#14 CACHED
对 Go 来说,同样的思路适用于模块缓存和构建缓存:
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd/server
缓存预热后,冷构建需要 13.6 秒的 Go 编译步骤只花了 5.3 秒——那还是在 arm64 QEMU 模拟下。在 amd64 上实际上是瞬时的。
用 docker buildx 做多架构构建
多架构构建生成一个 manifest(各平台镜像的列表),让 docker pull myimage:latest 透明地获取适合当前机器架构的镜像。如果你需要部署到 Intel/AMD 服务器和 ARM 机器的混合环境——树莓派、Graviton 实例、Apple Silicon Mac——你就需要这个。docker buildx build 命令接受 --platform 并处理其余一切:
docker buildx build --platform linux/amd64,linux/arm64 \
--tag buildkit-demo:multiarch \
--output type=oci,dest=./image.tar .
--output type=oci,dest=... 在本地写入 OCI 镜像 tar 包——不需要 registry,不需要推送。我运行它时,BuildKit 构建了两个平台并生成了 manifest list。amd64 侧全部 CACHED(来自之前的构建);arm64 侧在 QEMU 模拟下运行。以下是构建尾部输出:
#25 [linux/arm64 builder 7/7] RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked ...
#25 CACHED
#27 [linux/arm64 runtime 2/3] RUN --mount=type=cache,target=/var/cache/apt ...
#27 CACHED
#28 [linux/amd64 runtime 3/3] COPY --from=builder /out/app /usr/local/bin/app
#28 CACHED
#29 [linux/arm64 runtime 3/3] COPY --from=builder /out/app /usr/local/bin/app
#29 CACHED
#30 exporting to oci image format
#30 exporting manifest list sha256:a54358a5fcefe64cb87b6340bf3a0bea724c234ae8d88a37528d1dae8ee5443a 0.0s done
#30 sending tarball 1.6s done
#30 DONE 1.6s
生成的 OCI tar 包含一个 index.json——即 manifest list——指向各平台的 manifest:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:a54358a5fcefe64cb87b6340bf3a0bea724c234ae8d88a37528d1dae8ee5443a",
"size": 1607,
"annotations": {
"io.containerd.image.name": "docker.io/library/buildkit-demo:multiarch"
}
}
]
}
如果你有 registry,可以直接用 --push 推送,然后检查:
docker buildx imagetools inspect buildkit-demo:multiarch
这会打印包含两个平台及其 digest 的 manifest list。我用 --output type=oci 是因为不想推送到公共 registry——本地 tar 包给了我同样可检查的 manifest 结构。
模拟的真实代价
这是教程跳过的部分。在 amd64 上原生构建很快。在 amd64 主机上为 arm64 构建意味着每条指令都要经过 QEMU 模拟——而且很慢。我实测了:
| 步骤 | amd64(原生) | arm64(QEMU) |
|---|---|---|
apt-get install ca-certificates(runtime) | 冷构建 19 秒,之后缓存命中 | 169 秒 |
apt-get install ca-certificates git(builder) | 冷构建 32 秒,之后缓存命中 | 311 秒 |
go build(模块+构建缓存预热) | ~1 秒 | 5.3 秒 |
| 完整多架构构建(缓存预热) | — | >600 秒 — 超时 |
仅 arm64 的 apt-get 步骤就花了 169 秒和 311 秒——大约比 amd64 对应步骤慢 10 倍。即使缓存预热,Go 构建也花了 5.3 秒而不是约 1 秒。缓存冷启动的完整多架构构建超过了 10 分钟超时。这不是 BuildKit 的问题;这是模拟不同 CPU 架构的物理规律。如果你需要快速的 arm64 构建,使用真实的 arm64 机器(树莓派 4 对小镜像就够了)或有原生 arm64 支持的 CI runner——比如 GitHub Actions 提供 ubuntu-24.04-arm。
用 —progress=plain 调试构建
默认的 BuildKit 进度输出是紧凑的 TTY 显示,会折叠已完成的步骤。当构建失败时,你需要完整日志。--progress=plain 给你一切——每次 apt-get 拉取、每个编译警告、每次缓存未命中——以可滚动流的形式:
docker buildx build --progress=plain --tag myapp:latest .
我在本文的每次构建中都使用了它。这就像进度条和编译器日志的区别:调试时,你要的是日志。
构建中的密钥
最后一个值得了解的特性:构建密钥。如果你的构建需要私有 Git 仓库、npm token 或签名密钥,可以将其作为 secret mount 传递,而它不会出现在任何层中:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --omit=dev
docker buildx build --secret id=npmrc,src=$HOME/.npmrc --tag myapp:latest .
secret 仅在该 RUN 指令期间可用,永远不会被写入镜像或构建缓存。我用它来处理私有 Go 模块代理——将 .netrc 文件作为 secret 挂载的 GOPRIVATE 模式。
总结
BuildKit 不是新工具——它是你已经使用的工具的更好引擎。实践中重要的三件事:cache mount 让包管理器层在首次构建后基本免费,docker buildx —platform 为异构集群生成真正的多架构 manifest,--progress=plain 让你看到实际发生的事情。唯一需要尊重的是模拟的代价:在 amd64 上为 arm64 交叉编译对偶尔构建没问题,但如果你每天都这样做,给自己弄一台 arm64 机器或使用有 arm64 的 CI runner。现在我的所有镜像构建都在用 BuildKit——工作流一样,反馈回路快得多。如果你想在此基础上缩减镜像大小,我的容器镜像大小笔记是不错的下一篇阅读,Docker Compose cookbook有与这些镜像配套的服务级模式。