Most people know BuildKit as the thing that makes docker build fast. But BuildKit is a general-purpose build framework with a programmable architecture that can produce any artifact, not just container images. Here's how it works and how I used it to build Alpine APK packages.

大多数人每天都在和 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/

更多推荐