mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1811 字
5 分钟
镜像构建:BuildKit
2026-06-07

当你执行 docker build -t myapp . 时,Docker 读取 Dockerfile,逐条执行指令,每条指令生成一个镜像层,最终输出一个完整的容器镜像。但这个过程远比看起来复杂——BuildKit 需要解析 Dockerfile 语法、构建依赖图、并行执行无依赖的指令、管理缓存、处理多阶段构建。

BuildKit 是 Docker 的新一代构建引擎(Docker 23.0+ 默认启用),相比旧版 builder,它支持并行构建、缓存导入导出、secret 挂载、多平台构建等高级特性。理解 BuildKit 的工作原理,是编写高效 Dockerfile 和优化构建速度的基础。

镜像构建的演进是从”串行执行”到”并行 DAG”的飞跃。Docker 最初的 docker build 逐条执行 Dockerfile 指令,每条指令生成一个镜像层,严格串行——即使两条指令之间没有依赖关系,也必须等前一条执行完。2017 年,Tõnis Tiigi 提出 BuildKit 项目,引入 LLB(Low-Level Build)中间表示,将 Dockerfile 解析为有向无环图(DAG),无依赖的指令可以并行执行。BuildKit 还带来了 cache mount(--mount=type=cache)让包管理器缓存持久化、secret mount(--mount=type=secret)让构建时安全地使用密钥、多阶段构建优化让最终镜像更小。从 Docker 18.09 开始,BuildKit 作为实验特性可用,Docker 23.0 正式默认启用。理解 BuildKit 的演进,有助于理解为什么有些 Dockerfile 写法在旧版 builder 上很慢但在 BuildKit 上很快——因为 BuildKit 看到的是 DAG,不是线性指令列表。

前置知识#

  • Ch05 OCI 规范详解:镜像构建的产出是 OCI Image,理解 Image Spec 是理解构建过程的前提
  • Ch04 OverlayFS:容器文件系统:镜像层通过 OverlayFS 实现复用,理解分层存储有助于理解构建缓存
  • Dockerfile 基础:FROMRUNCOPYCMD 等基本指令
Note

本章假设你已写过基本的 Dockerfile。如果你是 Docker 新手,推荐先阅读 Docker 官方文档的 Dockerfile reference

一、镜像构建基础#

1.1 Dockerfile 指令与镜像层#

每条 Dockerfile 指令生成一个镜像层(Layer):

FROM ubuntu:22.04 # 层 1: 基础镜像
RUN apt-get update # 层 2: 包索引更新
RUN apt-get install -y nginx # 层 3: 安装 nginx
COPY nginx.conf /etc/nginx/ # 层 4: 复制配置
EXPOSE 80 # 元数据(不生成层)
CMD ["nginx", "-g", "daemon off;"] # 元数据(不生成层)
指令是否生成层说明
FROM基础镜像层
RUN执行命令,结果写入新层
COPY/ADD复制文件,结果写入新层
ENV设置环境变量(元数据)
EXPOSE暴露端口(元数据)
CMD/ENTRYPOINT启动命令(元数据)
WORKDIR工作目录(元数据)
ARG构建参数(元数据)

1.2 镜像层的存储#

# 查看镜像的层
docker history myapp
# IMAGE CREATED CREATED BY SIZE
# abc123 2 mins ago CMD ["nginx", "-g", "daemon off;"] 0B
# def456 2 mins ago EXPOSE 80 0B
# ghi789 2 mins ago COPY nginx.conf /etc/nginx/ 1.2kB
# jkl012 3 mins ago RUN apt-get install -y nginx 23.5MB
# mno345 3 mins ago RUN apt-get update 18.2MB
# pqr678 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
# 2 weeks ago 77.8MB
# 注意:RUN 指令生成的层最大,因为包含了文件系统变更

1.3 构建缓存#

Docker 构建时使用缓存加速——如果某条指令的输入未变,则复用之前的构建结果:

# 缓存命中规则:
# 1. FROM 指令:检查基础镜像是否变化
# 2. COPY/ADD:检查源文件的 checksum 是否变化
# 3. RUN:检查命令字符串是否变化(不检查执行结果!)
# 常见陷阱:RUN apt-get update 的缓存
# 如果 apt-get update 的命令字符串未变,Docker 会复用缓存
# 但软件包列表可能已更新!
# 解决方案:合并 RUN apt-get update && apt-get install

二、BuildKit 架构#

2.1 BuildKit 的核心组件#

graph TB subgraph 前端["前端 (Frontend)"] DOCKERFILE["Dockerfile 前端"] BUILDX["Buildx 前端"] end subgraph 核心引擎["BuildKit 核心引擎"] SOLVER["LLB Solver<br/>依赖图求解"] SCHEDULER["Scheduler<br/>并行调度"] CACHE["Cache Manager<br/>缓存管理"] end subgraph 执行器["执行器 (Executor)"] RUNC["runc 容器"] OCI["OCI 执行器"] end subgraph 导出器["导出器 (Exporter)"] DOCKER_EXPORT["Docker Image"] OCI_EXPORT["OCI Image"] REGISTRY["Registry Push"] LOCAL["Local Tar"] end DOCKERFILE --> SOLVER BUILDX --> SOLVER SOLVER --> SCHEDULER SOLVER --> CACHE SCHEDULER --> RUNC SCHEDULER --> OCI SOLVER --> DOCKER_EXPORT SOLVER --> OCI_EXPORT SOLVER --> REGISTRY SOLVER --> LOCAL style 前端 fill:#bbdefb,stroke:#1565c0 style 核心引擎 fill:#c8e6c9,stroke:#2e7d32 style 执行器 fill:#fff3e0,stroke:#e65100 style 导出器 fill:#e1bee7,stroke:#6a1b9a

2.2 LLB:线性构建图#

BuildKit 使用 LLB(Linear Build Graph)表示构建依赖关系。与旧版 builder 的线性执行不同,LLB 支持并行执行无依赖的指令:

graph TB FROM["FROM ubuntu"] --> RUN1["RUN apt-get update"] FROM --> COPY1["COPY package.json"] RUN1 --> RUN2["RUN apt-get install -y nginx"] COPY1 --> RUN3["RUN npm install"] RUN2 --> COPY2["COPY . /app"] RUN3 --> COPY2 COPY2 --> CMD["CMD [\"nginx\"]"] style FROM fill:#bbdefb,stroke:#1565c0 style RUN1 fill:#c8e6c9,stroke:#2e7d32 style COPY1 fill:#c8e6c9,stroke:#2e7d32 style RUN2 fill:#fff3e0,stroke:#e65100 style RUN3 fill:#fff3e0,stroke:#e65100

2.3 BuildKit vs 旧版 Builder#

特性旧版 BuilderBuildKit
并行构建串行执行自动并行
缓存导入/导出支持远程缓存
多阶段构建优化部分只构建需要的阶段
Secret 挂载—mount=type=secret
SSH 转发—mount=type=ssh
多平台构建—platform
增量构建—mount=type=cache
前端插件自定义前端

三、多阶段构建#

3.1 为什么需要多阶段构建#

单阶段构建的问题:构建工具(编译器、SDK)留在最终镜像中,导致镜像体积膨胀:

graph TB subgraph 单阶段["单阶段构建(~800MB)"] S1["golang:1.22 基础镜像<br/>~300MB"] S2["go mod download<br/>~200MB 依赖缓存"] S3["go build 产物<br/>~10MB 二进制"] S1 --> S2 --> S3 end subgraph 多阶段["多阶段构建(~15MB)"] M1["阶段1: golang:1.22<br/>编译 myapp"] M2["阶段2: alpine:3.19<br/>只复制二进制"] M1 -->|"COPY --from=builder<br/>只复制 /app/myapp"| M2 end 单阶段 -.->|"镜像体积 53x"| 多阶段 style 单阶段 fill:#ffcdd2,stroke:#c62828 style 多阶段 fill:#c8e6c9,stroke:#2e7d32
# 单阶段构建:镜像包含 Go 编译器(~300MB)
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
# 最终镜像大小:~800MB

3.2 多阶段构建#

# 阶段 1:构建
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
# 阶段 2:运行
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/myapp /usr/local/bin/myapp
EXPOSE 8080
CMD ["myapp"]
# 最终镜像大小:~15MB(只包含二进制 + Alpine)

3.3 多阶段构建的优化#

BuildKit 只构建被最终阶段引用的阶段:

# 多阶段构建的依赖图 FROM golang:1.22 AS builder # ... FROM node:20 AS frontend # ... FROM alpine:3.19 AS runtime COPY --from=builder /app/myapp /usr/local/bin/ # frontend 阶段不会被构建(因为没有被引用)

四、高级构建特性#

4.1 BuildKit 的 Mount 类型#

# 1. cache mount:持久化缓存目录
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -o myapp
# 2. secret mount:安全传递密钥
RUN --mount=type=secret,id=github_token \
git clone https://$(cat /run/secrets/github_token)@github.com/repo.git
# 3. ssh mount:SSH agent 转发
RUN --mount=type=ssh git clone git@github.com:repo.git
# 4. bind mount:只读挂载构建上下文
RUN --mount=type=bind,source=.,target=/src \
make -C /src

4.2 BuildKit 增量缓存实战#

BuildKit 的 --mount=type=cache 可以在构建之间持久化包管理器缓存,避免每次重新下载依赖:

# Go 模块缓存持久化
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o myapp
# pip 缓存持久化
FROM python:3.12 AS builder
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# npm 缓存持久化
FROM node:20 AS builder
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN --mount=type=cache,target=/root/.npm \
npm run build

4.3 缓存导入/导出#

flowchart LR subgraph 本地["本地开发"] LOCAL_BUILD["docker build"] LOCAL_CACHE["本地缓存<br/>~/.cache/buildkit"] end subgraph CI["CI/CD 流水线"] CI_BUILD["docker buildx build"] REGISTRY_CACHE["Registry 缓存<br/>myapp:cache"] GHA_CACHE["GitHub Actions<br/>cache action"] end subgraph 团队["团队协作"] TEAM_BUILD["docker buildx build"] S3_CACHE["S3 缓存<br/>s3://build-cache/"] end LOCAL_BUILD --> LOCAL_CACHE LOCAL_CACHE --> LOCAL_BUILD CI_BUILD --> REGISTRY_CACHE REGISTRY_CACHE --> CI_BUILD CI_BUILD --> GHA_CACHE GHA_CACHE --> CI_BUILD TEAM_BUILD --> S3_CACHE S3_CACHE --> TEAM_BUILD style 本地 fill:#bbdefb,stroke:#1565c0 style CI fill:#c8e6c9,stroke:#2e7d32 style 团队 fill:#fff3e0,stroke:#e65100
Tip

在 CI/CD 中使用 --cache-from=type=registry 可以将上一轮构建的缓存作为起点,避免每次从头开始。对于 Go 项目,配合 --mount=type=cache,target=/go/pkg/mod 持久化模块缓存,构建时间可以从 5 分钟降到 30 秒以内。

# 导出缓存到 Registry
docker buildx build \
--cache-to=type=registry,ref=myregistry/myapp:cache \
--cache-from=type=registry,ref=myregistry/myapp:cache \
-t myapp .
# 导出缓存到本地
docker buildx build \
--cache-to=type=local,dest=./cache \
--cache-from=type=local,src=./cache \
-t myapp .
# 导出缓存到 S3
docker buildx build \
--cache-to=type=s3,region=us-east-1,bucket=my-cache \
--cache-from=type=s3,region=us-east-1,bucket=my-cache \
-t myapp .

4.4 多平台构建#

# 创建多平台 builder
docker buildx create --name multiplatform --use
# 构建多平台镜像
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:latest \
--push .
# BuildKit 使用 QEMU 模拟非本机平台

五、Dockerfile 最佳实践#

5.1 层优化#

# 不好:每条 RUN 生成一个层
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get install -y redis
RUN apt-get clean
# 好:合并 RUN 指令,减少层数
RUN apt-get update && \
apt-get install -y nginx redis && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

5.2 缓存优化#

# 不好:代码变更导致 npm install 缓存失效
COPY . /app
RUN npm install
# 好:先复制 package.json,利用缓存
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app

5.3 安全优化#

# 使用非 root 用户
FROM alpine:3.19
RUN adduser -D appuser
USER appuser
COPY --chown=appuser:appuser . /app
WORKDIR /app
CMD ["./myapp"]

5.4 镜像大小优化对比#

优化方式镜像大小说明
基础镜像 ubuntu:22.0477.8MB未优化
基础镜像 alpine:3.197.3MB使用 Alpine
多阶段构建~15MB只复制二进制
静态编译 + scratch~5MB最小镜像
distroless~2MB无 shell 的最小镜像
# 最小镜像:静态编译 + scratch
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp
FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
# 最终镜像大小:~5MB(只包含静态链接的二进制)

六、动手实践#

6.1 构建优化实验#

#!/bin/bash
# 对比不同 Dockerfile 的构建时间和镜像大小
echo "=== 未优化的 Dockerfile ==="
cat > Dockerfile.bad << 'EOF'
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get install -y curl
COPY . /app
CMD ["nginx", "-g", "daemon off;"]
EOF
time docker build -t myapp:bad -f Dockerfile.bad .
docker images myapp:bad --format "{{.Size}}"
echo ""
echo "=== 优化后的 Dockerfile ==="
cat > Dockerfile.good << 'EOF'
FROM nginx:alpine AS base
FROM base AS builder
COPY --from=builder /etc/nginx/nginx.conf /etc/nginx/nginx.conf
COPY . /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
EOF
time docker build -t myapp:good -f Dockerfile.good .
docker images myapp:good --format "{{.Size}}"

6.2 BuildKit 缓存实验#

#!/bin/bash
# BuildKit 缓存实验
# 1. 启用 BuildKit
export DOCKER_BUILDKIT=1
# 2. 第一次构建(无缓存)
time docker build --no-cache -t myapp:v1 .
# 3. 修改源代码
echo "// changed" >> main.go
# 4. 第二次构建(利用缓存)
time docker build -t myapp:v2 .
# 应该比第一次快很多
# 5. 查看 BuildKit 缓存
docker buildx du
docker buildx prune

七、CI/CD 中的镜像构建#

7.1 GitHub Actions 构建示例#

.github/workflows/build.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64

7.2 构建缓存策略#

缓存策略说明适用场景
inline cache缓存嵌入镜像层简单场景
registry cache缓存存储在 RegistryCI/CD 流水线
local cache缓存存储在本地本地开发
GHA cacheGitHub Actions 缓存GitHub CI
S3 cache缓存存储在 S3大型团队

7.3 镜像安全扫描集成#

# 在 CI/CD 中集成 Trivy 扫描
trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:latest
# 只在发现高危漏洞时失败
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# 生成 SBOM(软件物料清单)
trivy image --format spdx-json --output sbom.json myapp:latest

7.4 镜像签名与验证#

# 使用 cosign 签名镜像
cosign sign --key cosign.key myapp:latest
# 验证镜像签名
cosign verify --key cosign.pub myapp:latest
# 在 Kubernetes 中强制签名验证(Kyverno)
# 策略:只允许运行已签名的镜像

附、实践:Dockerfile 优化对比#

本节通过对比未优化和优化后的 Dockerfile,展示多阶段构建和层合并的镜像瘦身效果。需要 Docker 环境。

附.1 未优化的 Dockerfile#

# Dockerfile.bad — 未优化
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
RUN pip3 install flask
COPY app.py /app/app.py
WORKDIR /app
CMD ["python3", "app.py"]

问题:

  • 每条 RUN 生成一个镜像层,5 条 RUN = 5 层
  • apt-get updateapt-get install 分开写,update 缓存留在镜像中
  • 使用 ubuntu:22.04(约 77MB)而非 alpine(约 7MB)
  • 开发工具(pip、gcc)留在最终镜像中

附.2 优化后的 Dockerfile#

# Dockerfile.good — 多阶段构建 + 层合并
FROM python:3.12-alpine AS builder
RUN pip install --no-cache-dir flask
FROM python:3.12-alpine
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY app.py /app/app.py
WORKDIR /app
CMD ["python3", "app.py"]

优化点:

  • 多阶段构建:builder 阶段安装依赖,最终镜像只复制 site-packages,不含 pip/编译工具
  • 基础镜像python:3.12-alpine(约 50MB)替代 ubuntu:22.04(约 77MB)
  • 层合并pip install 只有一层,不残留缓存
  • --no-cache-dir:不让 pip 保留下载缓存

附.3 优化原则速查#

优化手段效果示例
多阶段构建最终镜像不含构建工具COPY --from=builder
合并 RUN 指令减少镜像层数RUN apt-get update && apt-get install
小基础镜像减小基础层alpine 替代 ubuntu
.dockerignore排除无关文件排除 .gitnode_modules
--no-cache-dir不保留包管理器缓存pip install --no-cache-dir
BuildKit 缓存挂载构建时缓存持久化--mount=type=cache,target=/var/cache/apt

附.4 镜像分析工具#

# 查看镜像层信息
docker history myapp:latest
# 用 dive 深度分析每一层(需安装 dive)
dive myapp:latest
# 用 Trivy 扫描镜像漏洞
trivy image myapp:latest

八、本章小结#

上一章探讨了容器存储与数据持久化。

特性说明收益
并行构建LLB 依赖图自动并行构建速度提升 2-5x
多阶段构建分离构建和运行环境镜像大小减少 90%+
缓存导入/导出远程缓存共享CI/CD 构建加速
Secret 挂载安全传递密钥不泄露密钥到镜像
增量缓存—mount=type=cache包管理器缓存持久化
多平台构建—platform 多架构一次构建多平台镜像
Note

高效的 Dockerfile 遵循三个原则:减少层数(合并 RUN 指令)、利用缓存(把不变的指令放前面)、多阶段构建(只复制运行时需要的文件)。BuildKit 的并行构建和缓存机制让这些优化更加有效。


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

镜像构建:BuildKit
https://blog.souloss.com/posts/container-runtime/image-building/
作者
Souloss
发布于
2026-06-07
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
综合实战:构建一个迷你容器运行时
容器运行时 综合实战——用 Go 从零构建一个迷你容器运行时——实现 Namespace 隔离(PID/Mount/UTS/IPC/Network)、Cgroup 资源限制(CPU/内存)、OverlayFS 分层文件系统、OCI Bundle 解析,最终实现一个能运行容器的 minirunc。将前 15 章的知识融会贯通,从「理解原理」到「动手实现」。
2
系列导读
容器运行时 本系列从 Linux 内核的 Namespace、Cgroup、OverlayFS 出发,深入 OCI 规范、runc 源码、containerd 架构,再到容器安全、沙箱运行时、网络、存储、镜像构建、Wasm 容器,最后综合实战构建一个迷你容器运行时——从「会用 Docker」到「理解容器运行时的每一行代码」,每章配有可运行的代码示例与架构图,让你从容器用户进阶到容器运行时工程师。
3
容器存储
容器运行时 容器的可写层随容器删除而丢失,数据持久化需要 Volume。一网打尽容器存储的完整方案——Volume(绑定挂载/命名卷/tmpfs)、存储驱动(overlay2/devicemapper/btrfs)、CSI(容器存储接口)插件机制,以及 Kubernetes 的 PV/PVC/StorageClass 体系——从「docker run -v」到「理解容器存储的每一条挂载规则」。
4
containerd 架构
容器运行时 containerd 是工业级容器运行时管理器,是 Docker 和 Kubernetes 的核心依赖。从零讲透 containerd 的架构设计——gRPC API 服务、镜像管理(pull/unpack/mount)、任务管理(create/start/kill)、插件体系、事件系统,以及 containerd 如何在 runc 之上构建完整的容器生命周期管理能力。
5
容器网络
容器运行时 容器网络的核心问题是——隔离的 Network Namespace 如何与外部通信?详细解读 veth pair(虚拟网卡对)、bridge(虚拟网桥)、iptables/NAT(地址转换)、CNI(容器网络接口)的完整链路,以及 Docker 的四种网络模式和 Kubernetes 的 Pod 网络模型——从「容器能 ping 通外网」到「理解每一条网络规则」。