本文翻译自 BuildKit: Docker’s Hidden Gem,原载于 Hacker News。
大多数开发者每天都在使用 BuildKit,却从未意识到它的存在。当你执行 docker build 时,幕后工作的正是 BuildKit。但如果把 BuildKit 仅仅理解为「构建 Dockerfile 的工具」,就像把 LLVM 说成「编译 C 语言的工具」一样——这严重低估了它的架构价值。
BuildKit 是一个通用的、可插拔的构建框架。它当然可以生成 OCI 镜像,但同样可以输出 tar 包、本地目录、APK 包、RPM 包,或者任何你能用有向无环图(DAG)描述的文件系统操作。Dockerfile 只是它的一个前端而已。你可以编写自己的前端。
架构解析
BuildKit 的设计简洁清晰,理解了它的分层结构后会发现其实相当直观。核心概念有三个。
LLB:中间表示层
BuildKit 的核心是 LLB(Low-Level Build definition)。可以把它理解为构建系统领域的 LLVM IR。LLB 是一个二进制协议(基于 protobuf),描述了文件系统操作的 DAG:运行命令、复制文件、挂载文件系统。它是内容寻址的,意味着相同的操作产生相同的哈希值,从而实现激进的缓存策略。
当你编写 Dockerfile 时,Dockerfile 前端解析它并生成 LLB。但 BuildKit 并不要求输入必须是 Dockerfile——任何能产生有效 LLB 的程序都可以驱动 BuildKit。
Frontend:自带语法
Frontend(前端) 是一个容器镜像,BuildKit 运行它来将你的构建定义(Dockerfile、YAML、JSON、HCL,任意格式)转换为 LLB。前端通过 BuildKit Gateway API 接收构建上下文和构建文件,然后返回序列化的 LLB 图。
关键洞察在于:构建语言不是固化在 BuildKit 里的,而是一个可插拔层。你可以编写一个读取 YAML 规范、TOML 配置或自定义 DSL 的前端,BuildKit 会像执行 Dockerfile 一样执行它。
你其实已经见过这个机制了。Dockerfile 顶部的 # syntax= 指令告诉 BuildKit 使用哪个前端镜像。# syntax=docker/dockerfile:1 只是默认值。你可以指向任意镜像。
Solver 和 Cache:内容寻址执行
Solver(求解器) 接收 LLB 图并执行它。DAG 中的每个顶点都是内容寻址的,所以如果你之前已经用相同的输入构建过某个步骤,BuildKit 会直接跳过。这就是 BuildKit 快的原因:它不像旧的 Docker 构建器那样线性缓存层,而是在整个图的操作级别缓存,并且可以并行执行独立的分支。
缓存可以是本地的、内联的(嵌入到镜像中)或远程的(镜像仓库)。这使得 BuildKit 构建可重现,并且可以在 CI runner 之间共享。
不仅仅是镜像
BuildKit 的 --output 参数让这一切变得实用。你可以告诉 BuildKit 以以下方式导出结果:
type=image— 推送到镜像仓库(docker build的默认行为)type=local,dest=./out— 将最终文件系统导出到本地目录type=tar,dest=./out.tar— 导出为 tar 包type=oci— 导出为 OCI 镜像 tar 包
type=local 输出对于非镜像场景最为有趣。你的构建可以产出编译好的二进制文件、软件包、文档或其他任何东西,BuildKit 会把结果写入磁盘。不需要容器镜像。
Earthly、Dagger 和 Depot 等项目都构建在 BuildKit 的 LLB 之上。这是一个经过验证的模式。
实战:用自定义前端构建 APK 包
为了具体演示,我构建了 apkbuild:一个自定义 BuildKit 前端,它读取 YAML 规范并生成 Alpine APK 包。不涉及 Dockerfile。整个构建流水线——从源码编译到 APK 打包——都在 BuildKit 内部通过 LLB 操作完成。可以把它想象成 Chainguard melange 的简化版本。
我选择 YAML 是因为它熟悉,但规范可以是任何你想要的格式(JSON、TOML、自定义 DSL),只要你的前端能解析它。
我的包 YAML 规范长这样:
name: hello
version: "1.0.0"
epoch: "0"
url: https://example.com/hello
license: MIT
description: Minimal CMake APK demo
sources:
app:
context: {}
build:
source_dir: hello
就这样。没有 Dockerfile。没有 shell 脚本。BuildKit 通过自定义前端读取这个规范,然后生成 .apk 文件。
运行方式
构建前端镜像:
docker build -t tuananh/apkbuild -f Dockerfile .
然后用它构建 APK 包:
cd example
docker buildx build \
-f spec.yml \
--build-arg BUILDKIT_SYNTAX=tuananh/apkbuild \
--output type=local,dest=./out \
.
你应该能在 out 文件夹里看到生成的 APK 包。
BUILDKIT_SYNTAX 告诉 BuildKit 使用我们的自定义前端,而不是默认的 Dockerfile 解析器。--output type=local 把生成的 .apk 文件导出到 ./out。没有镜像被创建。不涉及镜像仓库。
为什么这很重要
BuildKit 免费为你提供了一个内容寻址、并行化、可缓存的构建引擎。你不需要重新发明缓存、并行性或可重现性。你只需要编写一个将你的规范转换为 LLB 的前端,BuildKit 处理剩下的一切。
这不仅仅是玩具示例。Dagger 使用 LLB 作为其 CI/CD 流水线的执行引擎。Earthly 将 Earthfile 编译为 LLB。这个模式已经在生产规模上得到验证。
如果你正在构建一个需要编译代码、生成制品或编排多步骤构建的工具,考虑把 BuildKit 作为你的执行后端。Dockerfile 只是默认前端。真正的力量在于底层的引擎。
要点总结
- BuildKit ≠ Docker 构建器:它是一个通用的构建框架,Dockerfile 只是众多前端之一
- LLB 是核心:类似 LLVM IR 的中间表示,内容寻址,支持激进缓存
- 前端可插拔:可以用 YAML、TOML、自定义 DSL 作为构建定义语言
- 输出多样化:不仅限于镜像,可以输出本地目录、tar 包、软件包等
- 实战项目:Dagger、Earthly、Depot 都在用 LLB 作为执行引擎
对于需要构建复杂制品的场景,BuildKit 是一个值得深入学习的工具。