Buildx Bake 与多目标构建编排

讲解 docker buildx bake 如何用一个声明式文件替代一长串 docker buildx build 命令:HCL/JSON/Compose 三种文件格式与解析优先级、targets/groups/inherits/matrix 语法、变量与上下文注入、多平台与缓存导出配置,以及在 CI 中按需构建、按目标复用的工程实践与调试技巧。

当一个仓库里有五六个镜像、每个都要针对两种架构构建、还要分别推送到不同 registry 时,docker buildx build 的参数会迅速膨胀成难以维护的复制粘贴。Bake 的价值就在于把这堆参数收敛进一个声明式文件:一次定义、按目标调用、在 CI 里只构建变更的部分。本篇讲清 Bake 的文件格式、编排语义、变量体系与 CI 落地方式。

1. 从命令行参数到声明式文件

先看 Bake 要解决的原始问题。没有 Bake 时,多目标构建通常写成脚本:

docker buildx build --target api   -t reg/api:$TAG  --platform linux/amd64,linux/arm64 --push ./api
docker buildx build --target web   -t reg/web:$TAG  --platform linux/amd64,linux/arm64 --push ./web
docker buildx build --target worker -t reg/worker:$TAG --platform linux/amd64,linux/arm64 --push ./worker

问题有三:平台与 tag 规则重复、缓存配置无法统一、CI 中无法只构建改动的那一个。Bake 把这三点都收进文件。

# 默认按顺序查找这些文件
docker buildx bake --print
# docker-compose.yml / docker-compose.yaml / docker-bake.json
# docker-bake.hcl / docker-bake.override.hcl

调用方式很直接:

docker buildx bake                 # 构建所有 target
docker buildx bake api             # 只构建 api
docker buildx bake api web         # 构建多个
docker buildx bake --push          # 全局追加 push
docker buildx bake --set api.tags=reg/api:dev   # 临时覆盖

1.1 解析优先级

Bake 会合并多个文件,顺序与优先级是:

优先级文件说明
低docker-compose.yml取 services.*.build 作为 target
中docker-bake.jsonJSON 形式,便于程序生成
高docker-bake.hclHCL 形式,支持变量与函数
最高docker-bake.override.hcl环境相关覆盖,通常不入库

同名 target 后者覆盖前者的字段,未冲突的字段做合并。-f 可显式指定文件并改变顺序:

docker buildx bake -f docker-bake.hcl -f ci.hcl --print

2. HCL 基础:target、group 与变量

HCL 是 Bake 的主力格式。一个最小可用的文件如下:

variable "TAG" {
  default = "dev"
}

variable "REGISTRY" {
  default = "registry.example.com"
}

group "default" {
  targets = ["api", "web"]
}

target "api" {
  context    = "./api"
  dockerfile = "Dockerfile"
  tags       = ["${REGISTRY}/api:${TAG}"]
  platforms  = ["linux/amd64", "linux/arm64"]
}

target "web" {
  context = "./web"
  tags    = ["${REGISTRY}/web:${TAG}"]
}

几个关键点:

  • group "default" 决定不带参数执行 bake 时构建哪些 target;不定义时默认构建全部 target。
  • variable 可被环境变量覆盖:环境变量 TAG=v1.2.3 会自动覆盖 variable "TAG",无需额外配置。
  • 变量插值用 ${VAR},字符串拼接直接写在引号里;注意 HCL 的 ${ 需要转义时写成 $${。
TAG=v1.2.3 docker buildx bake --print
# 输出解析后的完整构建计划,用于确认变量替换结果

2.1 target 的常用字段

字段等价 CLI说明
context位置参数构建上下文
dockerfile-fDockerfile 路径
target–target多阶段构建的阶段名
tags-t镜像标签列表
platforms–platform目标平台列表
args–build-arg构建参数 map
cache-from–cache-from缓存来源
cache-to–cache-to缓存导出
output-o输出类型(image/local/registry)
labels–labelOCI 标签
secrets–secret构建期密钥
ssh–ssh转发 SSH agent

字段名与 CLI 的映射关系并非一一对应,docker buildx bake --print 是最权威的对照工具——它输出的 JSON 就是 Bake 实际会传给 BuildKit 的配置。

3. inherits:用继承消除重复

多目标之间大量参数是共享的(平台、registry 前缀、缓存配置)。inherits 让 target 复用另一个 target 的字段:

target "base" {
  platforms = ["linux/amd64", "linux/arm64"]
  args = {
    GO_VERSION = "1.23"
  }
  cache-from = ["type=registry,ref=${REGISTRY}/cache:build"]
  cache-to   = ["type=registry,ref=${REGISTRY}/cache:build,mode=max"]
}

target "api" {
  inherits   = ["base"]
  context    = "./api"
  tags       = ["${REGISTRY}/api:${TAG}"]
}

target "worker" {
  inherits   = ["base"]
  context    = "./worker"
  tags       = ["${REGISTRY}/worker:${TAG}"]
}

继承的合并规则:

  • 列表类字段(platforms、tags、cache-from):子 target 的列表会追加到父列表之后,而非替换。想替换需显式清空或改用变量控制。
  • map 类字段(args、labels):逐 key 合并,同名 key 子级覆盖父级。
  • 标量字段(context、dockerfile):子级覆盖父级。

这条「列表追加」规则是最容易踩的坑:父 target 定义了 platforms,子 target 再加一个平台,结果是三个而不是两个。

target "api" {
  inherits  = ["base"]
  platforms = ["linux/amd64"]   # 结果仍是 amd64 + arm64 + amd64
}

3.1 用 matrix 批量生成 target

当一个镜像需要按「架构 × 变体」组合构建时,手写 target 会爆炸。matrix 按笛卡尔积自动展开:

target "app" {
  name = "app-${tgt}"
  matrix = {
    tgt = ["alpine", "debian"]
  }
  context    = "./app"
  dockerfile = "Dockerfile.${tgt}"
  tags       = ["${REGISTRY}/app:${TAG}-${tgt}"]
}

生成的 target 名为 app-alpine、app-debian,可通过 docker buildx bake app-alpine 单独调用。matrix 也支持多个维度:

target "runtime" {
  name = "rt-${arch}-${libc}"
  matrix = {
    arch = ["amd64", "arm64"]
    libc = ["glibc", "musl"]
  }
  platforms = ["linux/${arch}"]
  args = { LIBC = "${libc}" }
}

4. 变量体系与 CI 注入

Bake 的变量来源有四层,优先级从低到高:

  1. variable 块里的 default。
  2. 环境变量(同名即覆盖)。
  3. --set 命令行覆盖,形如 --set 'api.tags=reg/api:ci'。
  4. variable 块里的 validation 规则只做校验,不提供值。
# CI 中最常见的用法:用提交 SHA 与分支名打标签
export TAG="${CI_COMMIT_SHA:0:8}"
export REGISTRY="registry.example.com"
docker buildx bake --push

4.1 用函数做条件逻辑

HCL 支持有限的表达式,可用来做条件分支:

variable "PUSH" {
  default = false
}

target "api" {
  context = "./api"
  tags    = ["${REGISTRY}/api:${TAG}"]
  output  = PUSH ? ["type=registry"] : ["type=docker"]
}

注意 output 与 --push/--load 的交互:显式声明 output 后,--push 会被忽略,二者不应同时使用。CI 里推荐统一用 --push 控制,把 output 留给本地场景。

4.2 用 --set 做一次性覆盖

--set 不需要修改文件,适合 CI 的矩阵 job:

docker buildx bake --set 'api.platforms=linux/amd64' --set 'api.tags=reg/api:pr-123' api

--set 的键路径语法是 target.field,对列表字段是整体替换(与 inherits 的追加语义相反),这点必须记住,否则会出现「以为覆盖了,其实叠加了」。

5. 多平台与缓存导出

Bake 最大的价值在于把多平台与缓存配置集中定义,避免每个 build 命令各自为政。

target "release" {
  context   = "."
  dockerfile = "Dockerfile"
  platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"]
  tags      = ["${REGISTRY}/app:${TAG}", "${REGISTRY}/app:latest"]
  cache-from = [
    "type=registry,ref=${REGISTRY}/app:buildcache",
  ]
  cache-to = [
    "type=registry,ref=${REGISTRY}/app:buildcache,mode=max",
  ]
  provenance = "mode=max"
  sbom       = "true"
}

要点:

  • cache-to 的 mode=max 会导出所有中间阶段的层,缓存命中率更高但推送体积更大;mode=min 只导出最终阶段的层。CI 中通常选 max。
  • provenance 与 sbom 生成 attestation,推送时会额外生成 manifest 引用,需要 registry 支持 OCI 1.1 的 referrers 或 fallback tag 方案。
  • 多平台构建要求 builder 使用 docker-container 驱动,默认的 docker 驱动不支持:
docker buildx create --name multi --driver docker-container --bootstrap --use
docker buildx inspect --bootstrap

关于缓存模型与各种后端的取舍,见 构建缓存进阶 ;多平台与 manifest list 的细节见 多平台镜像构建与 Buildx 。

5.1 分平台缓存策略

不同平台可以指向不同的缓存位置,避免互相覆盖:

target "app-amd64" {
  inherits  = ["app"]
  platforms = ["linux/amd64"]
  cache-to  = ["type=registry,ref=${REGISTRY}/app:cache-amd64,mode=max"]
}

target "app-arm64" {
  inherits  = ["app"]
  platforms = ["linux/arm64"]
  cache-to  = ["type=registry,ref=${REGISTRY}/app:cache-arm64,mode=max"]
}

在 CI 里按 runner 架构分别构建再合并 manifest,比单机 QEMU 模拟构建快一个数量级。

6. CI 落地:按需构建与目标复用

Bake 在 CI 中的典型用法是「只构建变更的服务」。GitHub Actions 中可用路径过滤驱动:

name: build
on:
  push:
    paths:
      - 'api/**'
      - 'docker-bake.hcl'
jobs:
  bake:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ${{ vars.REGISTRY }}
          username: ${{ secrets.REG_USER }}
          password: ${{ secrets.REG_TOKEN }}
      - name: Bake
        env:
          TAG: ${{ github.sha }}
          REGISTRY: ${{ vars.REGISTRY }}
        run: docker buildx bake --push api

更进一步,可以让 Bake 自己判断:把 --print 的输出交给脚本,与上一次构建的 target 列表做差集。Bake 本身不提供变更检测,这层逻辑需要外层编排,通常结合 CI 的路径过滤或 monorepo 工具完成。相关模式可参考 CI 流水线中的容器构建 与 GitHub Actions 缓存优化 。

6.1 用 bake 统一本地与 CI

本地开发常需要 --load 进本地镜像库,CI 需要 --push。用变量区分即可,避免维护两份文件:

variable "OUTPUT_MODE" {
  default = "load"
}

target "app" {
  context = "."
  tags    = ["${REGISTRY}/app:${TAG}"]
  output  = ["type=${OUTPUT_MODE}"]
}
# 本地
docker buildx bake
# CI
OUTPUT_MODE=registry docker buildx bake

7. 调试与常见坑位

Bake 的报错信息往往指向 HCL 而非实际构建,掌握三个工具能省下大量时间:

# 1. 打印完整构建计划(最重要)
docker buildx bake --print > plan.json

# 2. 只构建并观察进度,不推送
docker buildx bake --progress=plain api

# 3. 校验 HCL 语法(不连 builder)
docker buildx bake --print -f docker-bake.hcl 2>&1 | head

常见问题与定位方式:

现象原因处理
failed to solve: no buildx builder用了默认 docker 驱动buildx create --driver docker-container
tags 出现重复条目inherits 列表追加语义用 --print 确认后改为变量控制
--set 覆盖无效键路径写错(应为 target.field)用 --print 对照
缓存始终 misscache-to 的 ref 被并发覆盖分平台或分 target 使用不同 ref
变量未替换用了 $VAR 而非 ${VAR}HCL 中统一用 ${}
CI 本地结果不一致本地用了 override 文件检查 docker-bake.override.hcl 是否入库

7.1 关于 override 文件

docker-bake.override.hcl 会被自动加载且优先级最高,适合放个人开发环境的覆盖(比如把 registry 指向本地)。务必把它写进 .gitignore,否则 CI 会意外加载个人配置,导致「本地能推、CI 推错仓库」的严重问题。

# .gitignore
docker-bake.override.hcl

8. 小结

Bake 的定位不是「另一个构建命令」,而是构建配置的单一事实来源:平台、标签、缓存、密钥、输出方式全部收敛进一个文件,命令行只负责选择目标与覆盖变量。落地时记住三条经验:

  • 用 inherits 抽公共字段,但警惕列表字段的追加语义,--print 是唯一可靠的验证手段。
  • 用 matrix 处理组合爆炸,用 --set 处理 CI 矩阵,两者配合可让一个文件覆盖全部流水线场景。
  • 把 docker-bake.override.hcl 排除在版本控制之外,避免个人配置污染 CI。

做到这三点,Bake 文件本身就能充当镜像交付清单,配合 provenance 与 sbom 开关生成的 attestation,可以形成从构建到准入的完整闭环。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「docker」更多文章

  1. 容器网络排障实战
  2. DinD/DooD 与临时 CI Runner
  3. 本地开发运行时:OrbStack 与 Colima