BuildKit:Docker 背后的构建引擎,其实能做得更多

大多数人每天都在和 BuildKit 打交道,却并没有意识到它的存在。你运行 docker build 的时候,背后的引擎就是 BuildKit。但如果把 BuildKit 简化成“那个用来构建 Dockerfile 的东西”,就像把 LLVM 说成“那个用来编译 C 的东西”一样——这会把它的架构能力低估一个数量级。
BuildKit 是一个通用、可插拔的构建框架。它当然可以产出 OCI 镜像,但也可以产出 tar 包、本地目录、APK 包、RPM,或者任何你能用“文件系统操作的有向无环图(DAG)”描述的东西。Dockerfile 只是其中一种 frontend(前端)。你也可以写自己的。
架构
BuildKit 的设计很干净,一旦看清各层之后甚至出人意料地容易理解。这里有三个关键概念。
LLB:中间表示
BuildKit 的核心是 LLB(Low-Level Build definition,低层构建定义)。你可以把它理解为构建系统里的 LLVM IR。LLB 是一种二进制协议(protobuf),用来描述一张由文件系统操作组成的 DAG:执行命令、拷贝文件、挂载文件系统等。它是内容寻址的,这意味着相同的操作会产生相同的哈希,从而实现激进的缓存。
当你写 Dockerfile 时,Dockerfile 前端会解析它并生成 LLB。但 BuildKit 并不要求输入一定是 Dockerfile。任何能生成合法 LLB 的程序,都可以驱动 BuildKit。
Frontends:自带语法
Frontend 是一个容器镜像,BuildKit 会运行它,把你的构建定义(Dockerfile、YAML、JSON、HCL,随便什么)转换成 LLB。Frontend 通过 BuildKit Gateway API 接收构建上下文和构建文件,然后返回序列化后的 LLB 图。
这里的关键洞见是:构建语言并没有“写死”在 BuildKit 里。它是一个可插拔层。你可以写一个 frontend 去读取 YAML 规范、TOML 配置,或者自定义 DSL,BuildKit 都会用同样的方式执行——就像执行 Dockerfile 一样。
其实你以前已经见过这个机制。Dockerfile 顶部的 # syntax= 指令会告诉 BuildKit 使用哪个 frontend 镜像。# syntax=docker/dockerfile:1 只是默认值。你可以把它指向任何镜像。
Solver 与缓存:内容寻址的执行
Solver(求解器)会接收 LLB 图并执行它。DAG 中每个顶点都是内容寻址的,所以如果你之前已经用相同输入构建过某一步,BuildKit 会直接跳过它。这就是 BuildKit 快的原因:它不只是像旧版 Docker builder 那样线性地缓存 layer,而是在整张图上按“操作级别”进行缓存,并且可以并行执行彼此独立的分支。
缓存可以是本地的、内联的(嵌在镜像里),或者远程的(例如 registry)。这让 BuildKit 的构建结果更可复现,也更方便在不同 CI runner 之间共享。

不只是镜像
BuildKit 的 --output 参数让这一切变得很实用。你可以告诉 BuildKit 把结果导出为:
• type=image —— 推送到 registry(这也是 docker build 的默认行为)
• type=local,dest=./out —— 把最终文件系统内容输出到本地目录
• type=tar,dest=./out.tar —— 导出为 tar 包
• type=oci —— 导出为 OCI 镜像 tar 包
对于“非镜像”的用例,最有意思的是 type=local 输出。你的构建可以产出编译后的二进制、软件包、文档或任何其他东西,BuildKit 会把结果直接落到磁盘上。不需要容器镜像。
像 Earthly、Dagger、Depot 这些项目,都是构建在 BuildKit 的 LLB 之上的。这是一种经过验证的模式。
用自定义 frontend 构建 APK 包
为了更具体地演示这一点,我做了 apkbuild:一个自定义的 BuildKit frontend,它读取一个 YAML 规格文件并生成 Alpine APK 包。全程不需要 Dockerfile。整个构建流水线——从源码编译到 APK 打包——都在 BuildKit 内部通过 LLB 操作完成。你可以把它看成 Chainguard 的 melange 的一个“玩具版”。
我选择 YAML 是因为大家更熟悉,但规格文件也可以是任何你喜欢的格式(JSON、TOML、自定义 DSL……),只要你的 frontend 能解析它即可。
我的包的 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。BuildKit 通过自定义 frontend 读取这个 spec,然后产出一个 .apk 文件。
运行方式
先构建 frontend 镜像:
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 使用我们自定义的 frontend,而不是默认的 Dockerfile 解析器。--output type=local 会把生成的 .apk 文件输出到 ./out。不会创建镜像,也不涉及 registry。
为什么这很重要
BuildKit 让你“免费”获得了一个内容寻址、可并行、可缓存的构建引擎。你不需要自己重新发明缓存、并行或可复现性。你只要写一个 frontend,把你的规格翻译成 LLB,剩下的交给 BuildKit。
这不仅适用于玩具 demo。Dagger 把 LLB 当作 CI/CD 流水线的执行引擎。Earthly 会把 Earthfile 编译成 LLB。这种模式已经在大规模场景中被证明可行。
如果你在做一个需要编译代码、产出构建产物、或编排多步骤构建流程的工具,考虑把 BuildKit 当成你的执行后端。Dockerfile 只是默认 frontend,真正的力量在下面那台引擎里。
翻译自:https://tuananh.net/2026/02/25/buildkit-docker-hidden-gem/
更多推荐
所有评论(0)