mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1923 字
5 分钟
镜像构建:BuildKit
2021-09-24

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

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

镜像构建从“串行执行”进化到“并行 DAG”。旧版 docker build 逐条执行 Dockerfile 指令,即使两条指令无依赖也必须等前一条完成。2017 年 BuildKit 引入 LLB(Low-Level Build)中间表示,将 Dockerfile 解析为有向无环图,无依赖的指令并行执行;同时带来 cache mount、secret mount、多阶段优化、多平台构建等特性。Docker 20.10 起内置 BuildKit(需设 DOCKER_BUILDKIT=1 启用),23.0 起改为默认使用 BuildKit。

前置知识#

Important
  • Ch05 OCI 规范详解:镜像构建的产出是 OCI Image,理解 Image Spec 是理解构建过程的前提

  • Ch04 OverlayFS:容器文件系统:镜像层通过 OverlayFS 实现复用,分层存储是构建缓存的基础

  • Dockerfile 基础:FROMRUNCOPYCMD 等基本指令

Note

本章假设你已写过基本的 Dockerfile,新手可先读 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(Low-Level Build)中间表示来表达构建依赖关系。与旧版 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.17 基础镜像<br/>~300MB"] S2["go mod download<br/>~200MB 依赖缓存"] S3["go build 产物<br/>~10MB 二进制"] S1 --> S2 --> S3 end subgraph 多阶段["多阶段构建(~15MB)"] M1["阶段1: golang:1.17<br/>编译 myapp"] M2["阶段2: alpine:3.14<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.17
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
# 最终镜像大小:~800MB

3.2 多阶段构建#

# 阶段 1:构建
FROM golang:1.17 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.14
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 只构建被最终阶段引用的阶段:

graph LR builder["builder<br/>golang:1.17"] frontend["frontend<br/>node:20"] runtime["runtime<br/>alpine:3.14"] builder -->|COPY --from=builder| runtime %% frontend 阶段没有被 runtime 引用,BuildKit 不会构建它 style frontend fill:#eeeeee,stroke:#999999,stroke-dasharray: 5,5,color:#999999 style runtime fill:#c8e6c9,stroke:#2e7d32 style builder fill:#bbdefb,stroke:#1565c0

frontend 阶段没有被最终阶段(runtime)通过 COPY --from=frontend 引用,BuildKit 在构建依赖图里找不到指向它的边,因此跳过不构建。只有 builder(被 runtime 引用)和 runtime(最终阶段)会实际执行。

四、高级构建特性#

4.1 BuildKit 的 Mount 类型#

# 1. cache mount:持久化缓存目录
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -o myapp
# 2. secret mount:安全传递密钥(用环境变量喂给 git,不要拼进 URL)
RUN --mount=type=secret,id=github_token \
GH_TOKEN=$(cat /run/secrets/github_token) git clone https://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.17 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.14
RUN adduser -D appuser
USER appuser
COPY --chown=appuser:appuser . /app
WORKDIR /app
CMD ["./myapp"]

5.4 镜像大小优化对比#

优化方式镜像大小说明
基础镜像 ubuntu<22>.0477.8MB未优化
基础镜像 alpine<3>.147.3MB使用 Alpine
多阶段构建~15MB只复制二进制
静态编译 + scratch~5MB最小镜像
distroless~2MB无 shell 的最小镜像
# 最小镜像:静态编译 + scratch
FROM golang:1.17 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 node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
# 第二阶段:只把构建产物放进运行镜像
FROM nginx:alpine
COPY --from=builder /app/dist /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}}"
# builder 阶段的 node_modules、npm 等都不会进入最终镜像,体积远小于 bad 版

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 优化对比#

Note

本节通过对比未优化和优化后的 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

减少层数、利用缓存、多阶段构建,三条原则背后是同一个目标:让构建可复现、让镜像可预测。合并 RUN 指令不仅减少层数,更重要的是消除中间状态的缓存污染(apt-get updateapt-get install 分开写就会遇到这个问题)。把不变指令放前面,本质上是把”变的部分”和”不变的部分”解耦,代码每天都在改,但依赖很少变。多阶段构建则把”构建时需要的”和”运行时需要的”严格分开,让最终镜像只包含运行必需的文件。


参考资料#

支持与分享

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

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

部分信息可能已经过时

相关文章 智能推荐
1
containerd 架构
容器运行时 containerd 是工业级容器运行时管理器,是 Docker 和 Kubernetes 的核心依赖。从零讲透 containerd 的架构设计,gRPC API 服务、镜像管理(pull/unpack/mount)、任务管理(create/start/kill)、插件体系、事件系统,以及 containerd 如何在 runc 之上构建完整的容器生命周期管理能力。
2
Cgroup v2 深入
容器运行时 Cgroup 是容器资源限制的核心机制。逐层拆解 Cgroup v2 的统一层级设计、CPU/内存/IO 三大控制器的实现原理、eBPF 扩展机制,以及容器运行时如何通过 Cgroup 实现资源隔离,从「docker run --memory=512m」到内核的 cgroup 文件系统,理解每一步的资源控制逻辑。
3
runc 源码分析
容器运行时 runc 是 OCI Runtime Spec 的参考实现,也是 Docker/Kubernetes 默认的低层容器运行时。从零讲透 runc 的源码架构,libcontainer 核心库、容器创建/启动的代码路径、Namespace/Cgroup/OverlayFS 的内核交互、安全配置(seccomp/AppArmor/Capabilities),让你从「知道 runc 是什么」到「理解 runc 的每一行关键代码」。
4
OverlayFS:容器文件系统
容器运行时 OverlayFS 是容器分层文件系统的核心。全面剖析 OverlayFS 的 lowerdir/upperdir/workdir 三层结构、whiteout 标记文件机制、copy-up 写时复制语义、多层叠加规则,以及容器运行时如何用 OverlayFS 实现镜像层复用和容器可写层,理解「为什么容器启动这么快」和「镜像层是怎么共享的」。
5
OCI 规范详解
容器运行时 OCI(Open Container Initiative)定义了容器镜像格式和运行时接口的开放标准。从零讲透 OCI 的三大规范,Image Spec(镜像格式、manifest、config、layer)、Runtime Spec(OCI Bundle、config.json、生命周期)、Distribution Spec(Registry API),理解容器生态标准化的基石,为阅读 runc 源码和理解 containerd 架构奠定基础。