Docker 基础体系 · 第 40/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker Buildx Bake:多目标、矩阵、缓存、平台和 CI 编排
docker buildx build 解决的是“一次构建一个目标”的问题:给定一个构建上下文、一个 Dockerfile、若干参数和平台,生成一个镜像或其他输出。docker buildx bake 则在此之上增加了一层声明式编排:把多个构建目标、目标之间的共同配置、矩阵展开、缓存策略、平台和输出统一写入一个配置文件,再由 BuildKit 执行。
这里的“目标”不是 Dockerfile 中的某一条指令,而是一次 BuildKit 构建请求。一个目标可以选择 Dockerfile 的某个 stage,也可以与其他目标共享 Dockerfile、上下文、缓存和构建图。
本文以现代 Docker Engine、Buildx、BuildKit 和 Compose 规范为背景,示例主要针对 Linux 容器镜像。Linux 容器可以在 Linux 主机上原生运行,也可以在其他系统上借助虚拟机或 Docker Desktop 运行;这些运行时边界不改变 Buildx 对 Linux 镜像的构建语义。Windows 容器镜像、Windows 容器节点和 Windows-specific Dockerfile 行为不在本文范围内。
一、Bake 到底解决什么问题
一个简单项目可能只有一个 Dockerfile:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag ghcr.io/acme/api:1.4.0 \
--push .
当项目出现以下需求时,命令行参数会迅速变得难以维护:
- 同一个仓库构建
api、worker、migrate三个镜像; - 它们共享上下文、Dockerfile 和基础构建阶段;
- 每个目标有不同的 Dockerfile stage、标签或构建参数;
- 需要为
amd64和arm64发布多架构镜像; - CI 中希望复用远程缓存;
- pull request 只验证构建,不推送生产标签;
- release 需要同时构建多个版本或多个变体。
Bake 将这些信息建模为目标集合和目标组:
Bake 配置
│
├── group:选择一批目标
│ │
│ ├── target api
│ ├── target worker
│ └── target migrate
│
└── 每个 target 转换为一个 BuildKit solve 请求
│
├── 解析 Dockerfile 和上下文
├── 查询本地或远程缓存
├── 构建 LLB 图
├── 并行执行可独立的节点
└── 写入 image、registry、cache 等输出
因此,Bake 不是新的镜像构建器。真正执行构建的是 BuildKit,Bake 主要负责:
- 读取和合并配置;
- 解析变量、继承和矩阵;
- 得到最终的目标集合;
- 将目标集合提交给 BuildKit;
- 根据目标的依赖和共享输入安排并发构建。
可以把一个 Bake target 抽象为:
其中:
- :构建上下文
context; - :Dockerfile 路径;
- :Dockerfile stage,即
target; - :构建参数和变量;
- :目标平台;
- :输出,例如镜像、OCI 布局或本地目录;
- :缓存输入和输出;
- :标签、secret、SSH 等元数据。
Bake 的主要价值是让一组 可以被声明、复用、展开和审查,而不是散落在多个 CI shell 脚本中。
二、最小 Bake 文件:target、group 和默认选择
在项目根目录创建 docker-bake.hcl:
target "api" {
context = "."
dockerfile = "Dockerfile"
target = "api"
tags = ["ghcr.io/acme/api:dev"]
}
target "worker" {
context = "."
dockerfile = "Dockerfile"
target = "worker"
tags = ["ghcr.io/acme/worker:dev"]
}
group "default" {
targets = ["api", "worker"]
}
对应的 Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
FROM build AS api
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api
FROM build AS worker
RUN CGO_ENABLED=0 go build -o /out/worker ./cmd/worker
FROM gcr.io/distroless/static-debian12 AS api-runtime
COPY --from=api /out/api /api
ENTRYPOINT ["/api"]
FROM gcr.io/distroless/static-debian12 AS worker-runtime
COPY --from=worker /out/worker /worker
ENTRYPOINT ["/worker"]
上面的 Bake target = "api" 选择的是 Dockerfile 中名为 api 的 stage,而不是最终的 api-runtime。如果希望构建可运行镜像,应改成:
target "api" {
context = "."
dockerfile = "Dockerfile"
target = "api-runtime"
tags = ["ghcr.io/acme/api:dev"]
}
这说明两个“target”容易混淆:
- Bake target:配置文件中的一个构建目标;
- Dockerfile target:
FROM ... AS name定义的 stage。
命令:
docker buildx bake
会选择名为 default 的 group。也可以显式选择目标:
docker buildx bake api
docker buildx bake api worker
或者选择 group:
docker buildx bake default
group 只负责组织目标,不直接定义 Dockerfile、缓存或平台。一个 group 可以包含多个 target,也可以包含其他 group,最终会展开为目标集合。
在执行真实构建前,应先查看 Bake 解析后的结果:
docker buildx bake --print
它会输出 JSON 形式的最终配置。这个步骤非常重要,因为变量、继承、override 文件和 --set 参数都会影响最终结果。排查“我写了配置但 BuildKit 没按预期执行”时,优先检查 --print,而不是直接猜测构建器行为。
三、HCL、JSON 和 Compose:Bake 配置从哪里来
常见配置文件包括:
docker-bake.hcldocker-bake.override.hcl- JSON 格式的 Bake 文件
- Compose 文件中的
build配置
使用 HCL 是因为它支持变量、表达式、函数和继承,适合复杂项目。Bake 也可以显式指定文件:
docker buildx bake -f docker-bake.hcl -f docker-bake.ci.hcl
多个文件被加载时,后加载的配置可以覆盖或补充前面的配置。项目通常把稳定配置放在 docker-bake.hcl,把本地或 CI 差异放在 override 文件中,但必须通过 --print 验证合并结果。
Compose 文件可以作为构建定义的输入。例如:
services:
api:
build:
context: .
dockerfile: Dockerfile
target: api-runtime
image: ghcr.io/acme/api:dev
worker:
build:
context: .
dockerfile: Dockerfile
target: worker-runtime
image: ghcr.io/acme/worker:dev
可以使用:
docker buildx bake -f compose.yaml
Compose 的 services.*.build 会被转换为相应的构建目标。但 Compose 的主要抽象是应用服务编排,Bake 的主要抽象是构建目标编排;当需要矩阵、复杂继承、远程缓存或精细的构建输出时,直接使用 docker-bake.hcl 通常更清晰。不要假设 Compose 中所有运行时字段都会影响镜像构建;例如 ports、depends_on 和容器网络配置不是 Dockerfile 构建步骤。
四、变量、继承和 --set
1. 变量
variable "REGISTRY" {
default = "ghcr.io/acme"
}
variable "TAG" {
default = "dev"
}
target "api" {
context = "."
dockerfile = "Dockerfile"
target = "api-runtime"
tags = ["${REGISTRY}/api:${TAG}"]
}
变量可用于集中控制注册表、版本和构建参数。CI 中可以通过环境变量或命令行覆盖,具体覆盖方式与 Buildx 版本和变量类型有关,因此应使用 --print 确认结果。例如:
REGISTRY=registry.example.com/acme TAG=1.4.0 \
docker buildx bake api --print
对于目标字段,也可以使用 --set 覆盖:
docker buildx bake api \
--set api.tags=registry.example.com/acme/api:1.4.0 \
--print
--set 的路径是 Bake target 的字段路径。它适合 CI 对标签、平台、构建参数做小范围覆盖,但复杂逻辑仍应放进版本控制中的 HCL 文件,否则 CI 配置会重新变成难以审查的命令行脚本。
2. 继承
多个目标通常有相同配置:
variable "REGISTRY" {
default = "ghcr.io/acme"
}
target "common" {
context = "."
dockerfile = "Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
cache-from = [
"type=registry,ref=${REGISTRY}/buildcache:main"
]
cache-to = [
"type=registry,ref=${REGISTRY}/buildcache:main,mode=max"
]
}
target "api" {
inherits = ["common"]
target = "api-runtime"
tags = ["${REGISTRY}/api:dev"]
}
target "worker" {
inherits = ["common"]
target = "worker-runtime"
tags = ["${REGISTRY}/worker:dev"]
}
inherits 表示把父 target 的字段带入子 target。子 target 可以覆盖字段,例如 api 和 worker 继承相同的平台与缓存,却有不同的 Dockerfile stage 和标签。
继承不是 Dockerfile stage 继承,也不意味着先构建父 target。它是配置合并关系。真正的执行仍由每个最终 target 的 Dockerfile 图决定。
一个重要边界是缓存引用的并发写入。如果多个并行目标同时向同一个远程缓存引用执行 cache-to,最终结果取决于导出过程和注册表行为,可能造成互相覆盖或产生不稳定的缓存内容。生产 CI 可以让多个目标共享只读的主分支缓存,同时为不同作业使用不同的写入引用,例如:
cache-from: registry.example.com/acme/buildcache:main
cache-to: registry.example.com/acme/buildcache:run-12345
周期性任务再把经过验证的缓存策略化为稳定引用。
五、矩阵:把一个声明展开成多个目标
矩阵的含义是:对一个 target 声明多个维度,Bake 为每个维度组合生成一个实际目标。
variable "REGISTRY" {
default = "ghcr.io/acme"
}
variable "TAG" {
default = "dev"
}
target "variant" {
matrix = {
flavor = ["slim", "debug"]
}
name = "variant-${flavor}"
context = "."
dockerfile = "Dockerfile"
target = "${flavor}"
tags = ["${REGISTRY}/app:${TAG}-${flavor}"]
}
Dockerfile:
# syntax=docker/dockerfile:1
FROM alpine:3.20 AS slim
COPY app /app
ENTRYPOINT ["/app"]
FROM alpine:3.20 AS debug
RUN apk add --no-cache curl
COPY app /app
ENTRYPOINT ["/app"]
执行:
docker buildx bake variant --print
逻辑上会得到两个目标:
variant-slim -> target=slim -> app:dev-slim
variant-debug -> target=debug -> app:dev-debug
矩阵的展开规则可以形式化为笛卡尔积。若有两个维度:
matrix = {
flavor = ["slim", "debug"]
version = ["1", "2"]
}
则目标集合是:
其中:
因此:
总目标数为:
D_i 是第 个维度的取值集合。两个 2 值维度产生 4 个目标,三个 5 值维度则产生 125 个目标。矩阵不是“自动提高并行度”的免费抽象;目标数量增加会增加调度、缓存导出、日志和注册表压力。
矩阵与多平台不是同一件事
下面两种写法语义不同:
target "release" {
platforms = ["linux/amd64", "linux/arm64"]
tags = ["ghcr.io/acme/app:1.0.0"]
}
这是一个目标、两个平台。BuildKit 可以生成一个多平台镜像索引,两个平台镜像使用同一个标签。
而下面是矩阵:
target "per-platform" {
matrix = {
platform = ["linux/amd64", "linux/arm64"]
}
name = "per-platform-${platform}"
platforms = ["${platform}"]
tags = ["ghcr.io/acme/app:${platform}"]
}
这是两个目标,每个目标只构建一个平台。它们不会因为标签相同或名称相似,就自动合并成多平台 manifest。若要发布一个统一的多架构标签,最简单的方式通常是直接使用 platforms = [...] 的单个目标;若必须拆成多个作业,则需要后续使用 manifest 工具或 docker buildx imagetools create 显式创建索引,并确保各平台镜像已经推送到注册表。
另外,平台字符串可能包含 /,不适合直接作为 Docker 标签的一部分。实际项目应使用安全的命名转换,或为平台目标使用单独的标签变量,而不是盲目把 linux/amd64 拼进 tag。
六、缓存:Bake 如何把策略交给 BuildKit
缓存不是 Bake 自己保存的“编译结果表”,而是 BuildKit 对构建图节点和文件输入进行匹配、复用和导出的机制。Bake 负责把 cache-from 和 cache-to 配置传给每个目标。
常见缓存后端包括:
inline:将部分缓存元数据写入镜像;registry:把缓存作为注册表中的独立引用;local:写入本地目录;gha:在 GitHub Actions 中使用 GitHub Actions Cache;- 其他后端是否可用取决于 BuildKit、Buildx 和运行环境。
一个适合 CI 的目标示例:
target "ci" {
context = "."
dockerfile = "Dockerfile"
target = "test"
cache-from = [
"type=registry,ref=ghcr.io/acme/app:buildcache"
]
cache-to = [
"type=registry,ref=ghcr.io/acme/app:buildcache,mode=max"
]
output = ["type=cacheonly"]
}
cacheonly 表示执行构建以验证构建过程,但不导出最终镜像。它适合 pull request 的构建检查;如果需要在本机加载镜像,应使用 type=docker 或命令行对应的本地输出方式,但多平台镜像通常不能直接加载到传统 Docker 镜像存储中。
Layer cache 与 cache mount 不是一回事
Dockerfile 中:
RUN npm ci
产生的是一个普通层。只要该指令以及它依赖的输入没有失效,BuildKit 可以复用整个 RUN 步骤。
而:
RUN --mount=type=cache,target=/root/.npm \
npm ci
使用的是 cache mount。它是供命令读取和写入的持久化缓存目录,不会直接成为最终镜像层。即使 npm ci 重新执行,已经下载的包也可能从 cache mount 中复用。
两者可以同时存在:
- Dockerfile 指令和输入未变化:整个
RUN层直接命中; - 指令失效但 cache mount 仍可用:命令重新执行,但下载过程可能更快;
- cache mount 也不存在:命令重新执行并重新下载。
这解释了为什么“层缓存失效”不等于“所有依赖都必须从网络重新下载”。
缓存失效通常由以下输入变化造成:
COPY或ADD的文件内容变化;- 前置 stage 的结果变化;
ARG或环境变量影响指令;- 基础镜像 digest 变化;
- Dockerfile 指令变化;
- 构建上下文中被纳入计算的文件变化。
一个常见反例是:
COPY . .
RUN npm ci
源码中任意一个文件变化,都可能使 COPY . . 及其后的 npm ci 失效。更适合依赖安装的顺序是:
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
这样,只有依赖清单变化时才需要重新执行依赖安装步骤。这里的收益来自输入依赖关系的改变,而不是 Bake 的特殊优化。
缓存命中不代表输出可发布
缓存复用只能说明某个构建图节点的输入被认为等价。它不自动证明:
- 使用的基础镜像仍符合当前安全要求;
- 构建产物包含了最新的源码;
- 构建结果适合当前目标平台;
- 缓存来自可信来源。
生产构建通常应固定关键基础镜像 digest,并在发布流程中执行漏洞扫描、签名或制品验证。缓存是性能机制,不是供应链信任机制。
七、平台、多架构和 BuildKit 构建器
1. platforms 的含义
target "release" {
context = "."
dockerfile = "Dockerfile"
target = "runtime"
platforms = [
"linux/amd64",
"linux/arm64"
]
tags = ["ghcr.io/acme/app:1.0.0"]
}
这里的目标平台集合表示最终镜像必须分别适用于 linux/amd64 和 linux/arm64。BuildKit 会为每个平台解析基础镜像、执行对应构建步骤,并最终生成一个镜像索引。
构建器必须支持这些平台:
docker buildx ls
docker buildx inspect --bootstrap
如果构建器只支持 linux/amd64,请求 linux/arm64 会失败,除非构建器通过 QEMU 模拟或连接了原生 arm64 节点。
2. 原生节点与 QEMU
跨平台构建有三种常见执行方式:
- 在目标架构的原生节点上构建;
- 在当前节点上通过 QEMU 模拟目标架构;
- 将两者组合起来,例如 amd64 原生节点负责 amd64,arm64 节点负责 arm64。
QEMU 的优点是配置简单,缺点是某些编译、压缩或测试步骤会明显变慢,并且可能暴露与模拟相关的兼容性问题。原生节点通常更稳定,但需要维护多节点 BuildKit builder、网络和凭据。
这不是“Bake 选择哪种架构”的问题。Bake 只声明 platforms;实际执行能力由当前 Buildx builder 决定。
3. BUILDPLATFORM 与 TARGETPLATFORM
Dockerfile 可以区分构建机平台和目标平台:
FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -o /out/app ./cmd/app
FROM --platform=$TARGETPLATFORM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
在多平台构建中:
BUILDPLATFORM是执行该 stage 的平台;TARGETPLATFORM是最终要生成的镜像平台;TARGETOS、TARGETARCH是拆分后的目标系统和架构。
对于纯 Go 交叉编译,构建 stage 可以在构建机平台运行 Go 编译器,再产生目标架构二进制。对于必须执行目标架构程序的步骤,例如运行目标程序生成代码,仍可能需要 QEMU 或原生节点。
一个危险的写法是:
FROM --platform=linux/amd64 ubuntu:24.04
如果目标包括 linux/arm64,这会强制所有目标使用 amd64 基础镜像。即使构建过程侥幸完成,最终镜像也无法正确作为 arm64 镜像运行。除非确实需要固定平台,否则应让 BuildKit 根据目标平台解析 FROM。
4. 输出方式决定能否发布
多平台目标通常应直接推送:
target "release" {
platforms = ["linux/amd64", "linux/arm64"]
tags = ["ghcr.io/acme/app:1.0.0"]
output = ["type=registry"]
}
等价命令通常是:
docker buildx bake release --push
--push 会把镜像和多平台索引推送到注册表。--load 适合把单平台结果加载到本地 Docker 镜像存储;传统本地镜像存储不能直接容纳一个多平台镜像索引,因此多平台发布不能简单地用 --load 替代 --push。
如果 CI 只执行:
docker buildx bake release
而没有 --push 或显式输出,镜像可能只存在于 BuildKit 缓存中,后续运行环境无法从注册表拉取。这是“构建成功但部署找不到镜像”的典型原因。
八、多个目标如何共享 Dockerfile 和构建图
考虑以下 Dockerfile:
FROM node:22 AS deps
WORKDIR /src
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
FROM deps AS build-api
COPY . .
RUN npm run build:api
FROM deps AS build-worker
COPY . .
RUN npm run build:worker
FROM nginx:1.27-alpine AS api-runtime
COPY --from=build-api /src/dist/api /usr/share/nginx/html
FROM node:22-alpine AS worker-runtime
COPY --from=build-worker /src/dist/worker /app
CMD ["node", "/app/index.js"]
Bake:
target "base" {
context = "."
dockerfile = "Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
}
target "api" {
inherits = ["base"]
target = "api-runtime"
tags = ["ghcr.io/acme/api:dev"]
}
target "worker" {
inherits = ["base"]
target = "worker-runtime"
tags = ["ghcr.io/acme/worker:dev"]
}
group "default" {
targets = ["api", "worker"]
}
BuildKit 会把每个目标转成构建图。两个目标都依赖 deps,因此在内容和平台都相同的情况下,BuildKit 可以复用相同节点;它不需要因为存在两个镜像标签,就重复下载同一依赖。
但“共享 Dockerfile”不等于“必然只执行一次”。以下情况会使节点无法复用:
- 两个目标使用不同平台;
- 构建参数不同;
- 上下文内容不同;
- 前置 stage 的输入不同;
- Dockerfile 实际引用了不同的 stage;
- 一个目标使用了不同的 secret、网络或安全设置。
目标之间没有显式的执行先后关系。Bake 会提交目标集合,BuildKit 根据依赖图和资源能力调度。能够并行的目标可能并行执行,因此多个目标共享一个远程 cache ref 时要考虑导出竞争。
九、CI 中的一个完整编排示例
假设 CI 需要满足:
- pull request:构建测试目标,不推送应用镜像;
- main 分支:构建并推送
edge标签; - release:构建
linux/amd64和linux/arm64,推送版本标签; - 所有流程复用主分支缓存。
Bake 文件:
variable "REGISTRY" {
default = "ghcr.io/acme"
}
variable "TAG" {
default = "dev"
}
target "common" {
context = "."
dockerfile = "Dockerfile"
cache-from = [
"type=registry,ref=${REGISTRY}/app:buildcache"
]
}
target "test" {
inherits = ["common"]
target = "test"
output = ["type=cacheonly"]
}
target "release" {
inherits = ["common"]
target = "runtime"
platforms = ["linux/amd64", "linux/arm64"]
tags = ["${REGISTRY}/app:${TAG}"]
cache-to = [
"type=registry,ref=${REGISTRY}/app:buildcache,mode=max"
]
output = ["type=registry"]
}
group "default" {
targets = ["test"]
}
CI 中可以执行:
# pull request:只验证构建
docker buildx bake test --progress=plain
# main:推送 edge
docker buildx bake release \
--set release.tags=ghcr.io/acme/app:edge \
--push \
--progress=plain
# release:推送版本
TAG=1.4.0 docker buildx bake release \
--push \
--progress=plain
每一步的因果关系如下:
test选择 Dockerfile 的teststage;output = ["type=cacheonly"]不创建可部署镜像;release选择runtimestage;platforms要求两个 Linux 平台;output = ["type=registry"]让结果进入注册表;--push是命令行对 registry 输出的直接启用方式;TAG影响标签字符串,但不会自动改变 Dockerfile 内的版本逻辑;cache-from让构建尝试读取已有远程缓存;cache-to将本次构建的缓存导出到远程引用。
在 CI 中,必须先完成以下前置操作:
docker buildx version
docker buildx ls
docker buildx inspect --bootstrap
并完成注册表登录:
docker login ghcr.io
若使用 GitHub Actions,通常还需要配置 Buildx builder、登录 action 和适当的 GITHUB_TOKEN 权限。权限不足时,构建本身可能成功,但在 cache export 或 image push 阶段失败。
十、构建目标中的 secrets、SSH 和敏感数据
构建时需要私有依赖时,不应把凭据写入 ARG 或普通环境变量:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
Bake target:
target "private-build" {
context = "."
dockerfile = "Dockerfile"
target = "runtime"
secret = [
"id=npmrc,src=${HOME}/.npmrc"
]
}
执行:
docker buildx bake private-build
Secret mount 的设计目标是让凭据只在对应构建步骤可见,不把它作为普通文件复制进最终镜像层。但如果 Dockerfile 主动执行:
RUN cat /run/secrets/npmrc > /app/npmrc
凭据仍可能进入层或构建产物。BuildKit 不会阻止应用显式泄露 secret。
同理,SSH mount 适用于需要通过 SSH 访问私有 Git 仓库的构建步骤,但它要求 builder 能获得对应 SSH agent 或密钥,且网络访问路径必须可用。CI 中应把 secret、SSH、网络权限和缓存来源一起纳入信任边界,而不能仅因为“没有写进 Dockerfile”就认为整个构建完全无敏感信息风险。
十一、故障诊断:从最终配置到 BuildKit 日志
1. 目标没有被执行
现象:
no targets specified
或执行成功但什么都没构建。
检查:
docker buildx bake --print
确认是否存在:
group "default" {
targets = ["..."]
}
也可以显式指定 target:
docker buildx bake api
如果配置文件名称非默认名称,必须使用:
docker buildx bake -f path/to/file.hcl api
2. 平台不受支持
现象通常类似:
no match for platform in manifest
或 builder 报告不支持目标平台。
诊断:
docker buildx inspect --bootstrap
检查 builder 的 Platforms 列表。若没有目标平台,需要:
- 使用支持该平台的原生节点;
- 正确安装并启用 QEMU;
- 创建包含多个节点的 Buildx builder;
- 或暂时减少
platforms集合。
不要通过强行写死 FROM --platform=linux/amd64 来“修复” arm64 构建;这只是掩盖 builder 能力或 Dockerfile 兼容性问题。
3. 缓存没有命中
先用:
docker buildx bake release --progress=plain
观察每一步是否显示缓存复用。然后检查:
--print中是否真的存在cache-from;- CI 是否登录了包含缓存引用的注册表;
- cache ref 是否被错误地当成镜像 tag 使用;
- Dockerfile 的
COPY顺序是否导致过早失效; - 构建参数、平台或基础镜像是否发生变化;
- 远程缓存是否被其他并行任务覆盖;
cache-to是否执行到了导出阶段。
缓存 miss 不一定是故障。基础镜像 digest 变化、依赖文件变化或目标平台变化,都可能使命中条件不成立。真正需要诊断的是“本应相同的输入为何不相同”。
4. 构建成功但镜像不存在
如果没有 --push,也没有 type=registry、type=docker 等有效输出,结果可能只停留在 BuildKit builder 中。检查目标的 output:
docker buildx bake release --print
发布到注册表时,使用:
docker buildx bake release --push
验证:
docker buildx imagetools inspect ghcr.io/acme/app:1.4.0
输出中应能看到多个平台的 manifest。若只有一个平台,说明矩阵目标没有被合并,或某个平台推送失败。
5. 多个目标的 tag 相互覆盖
矩阵或多目标配置中,如果多个目标使用相同 tag:
tags = ["ghcr.io/acme/app:dev"]
它们可能同时尝试更新同一注册表引用。最终标签指向哪个 manifest,不应依赖构建完成顺序。解决方式是:
- 为每个变体使用唯一 tag;
- 先分别推送不可变平台或变体引用;
- 再显式创建统一 manifest;
- 发布阶段使用版本号或 digest,而不是让并发任务争抢可变标签。
十二、常见误解和真实边界
误解一:Bake 会自动按 Dockerfile stage 顺序构建全部镜像
不会。Bake 只构建被选中的 target。Dockerfile 中未被选中且不被依赖的 stage 不会因为“写在那里”就自动构建。
误解二:两个 target 共用 Dockerfile,就一定完全共享缓存
不一定。缓存键与平台、参数、输入内容、前置图和指令有关。共享文本文件只是共享缓存的必要条件之一,不是充分条件。
误解三:矩阵会自动生成多架构 manifest
不会。矩阵生成多个独立 target。platforms = ["linux/amd64", "linux/arm64"] 才是单个 target 的多平台构建声明。拆分后的平台镜像需要额外合并。
误解四:--platform 只影响最终镜像标签
不会。它会影响基础镜像解析、构建步骤的执行平台、编译目标、缓存键和最终 manifest。错误的平台声明可能直到部署到另一种 CPU 架构时才暴露。
误解五:远程缓存等同于远程制品
不会。cache ref 保存的是供 BuildKit 复用的构建缓存元数据和内容;它不是面向部署的镜像标签,也不应作为生产发布物的替代品。
误解六:Linux 容器包含一个完整 Linux 内核
不会。Linux 容器通常共享运行它的 Linux 内核,只隔离用户空间、进程、网络和文件系统视图。Buildx 可以构建 Linux 用户空间镜像,但镜像能否运行仍取决于目标节点的内核、架构和容器运行时能力。
十三、如何选择 Bake 的建模方式
可以按以下因果关系选择配置:
- 只有一个镜像、一次构建:直接使用
docker buildx build更简单; - 多个 Dockerfile stage 生成多个镜像:使用多个 Bake target;
- 多个目标有公共 context、平台或缓存:使用
inherits; - 需要组合多个变体或版本:使用
matrix; - 同一标签要发布多个 CPU 架构:优先使用一个 target 的
platforms; - CI 需要按场景选择构建集合:使用多个
group; - 需要审查实际传给 BuildKit 的参数:使用
--print; - 需要远程缓存:在 target 中明确写
cache-from和cache-to; - 需要发布可拉取镜像:明确使用 registry output 或
--push。
一个可维护的 Bake 文件,最终应能回答四个问题:
- 这个目标选择了哪个 Dockerfile stage?
- 它会为哪些平台构建?
- 它从哪里读取缓存、向哪里写入缓存?
- 构建结果会输出到哪里,使用什么不可歧义的标签?
只要这四个问题无法从 docker-bake.hcl 和 docker buildx bake --print 中直接回答,CI 中就仍然存在隐藏状态。Bake 的核心不是减少命令行字符,而是把多目标构建从隐式脚本调用,转化为可检查的目标图、平台集合、缓存边界和输出契约。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Vue 与 React 静态站点镜像:构建、Nginx、缓存、路由和运行时配置
- 下一篇:Docker 构建证明:Provenance、SBOM、Attestation 和 SLSA
- 延伸:Docker 多架构构建:Buildx、QEMU、Manifest 与跨平台发布
- 延伸:BuildKit 缓存深入:Layer、Cache Mount、远程缓存和失效诊断
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论