当你执行 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 基础:
FROM、RUN、COPY、CMD等基本指令
本章假设你已写过基本的 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: 安装 nginxCOPY 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 的核心组件
2.2 LLB:线性构建图
BuildKit 使用 LLB(Linear Build Graph)表示构建依赖关系。与旧版 builder 的线性执行不同,LLB 支持并行执行无依赖的指令:
2.3 BuildKit vs 旧版 Builder
| 特性 | 旧版 Builder | BuildKit |
|---|---|---|
| 并行构建 | 串行执行 | 自动并行 |
| 缓存导入/导出 | 支持远程缓存 | |
| 多阶段构建优化 | 部分 | 只构建需要的阶段 |
| Secret 挂载 | —mount=type=secret | |
| SSH 转发 | —mount=type=ssh | |
| 多平台构建 | —platform | |
| 增量构建 | —mount=type=cache | |
| 前端插件 | 自定义前端 |
三、多阶段构建
3.1 为什么需要多阶段构建
单阶段构建的问题:构建工具(编译器、SDK)留在最终镜像中,导致镜像体积膨胀:
# 单阶段构建:镜像包含 Go 编译器(~300MB)FROM golang:1.22WORKDIR /appCOPY . .RUN go build -o myappCMD ["./myapp"]# 最终镜像大小:~800MB3.2 多阶段构建
# 阶段 1:构建FROM golang:1.22 AS builderWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o myapp
# 阶段 2:运行FROM alpine:3.19RUN apk --no-cache add ca-certificatesCOPY --from=builder /app/myapp /usr/local/bin/myappEXPOSE 8080CMD ["myapp"]# 最终镜像大小:~15MB(只包含二进制 + Alpine)3.3 多阶段构建的优化
BuildKit 只构建被最终阶段引用的阶段:
四、高级构建特性
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 /src4.2 BuildKit 增量缓存实战
BuildKit 的 --mount=type=cache 可以在构建之间持久化包管理器缓存,避免每次重新下载依赖:
# Go 模块缓存持久化FROM golang:1.22 AS builderWORKDIR /appCOPY go.mod go.sum ./RUN --mount=type=cache,target=/go/pkg/mod \ go mod downloadCOPY . .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 builderCOPY requirements.txt .RUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements.txtCOPY . .CMD ["python", "app.py"]
# npm 缓存持久化FROM node:20 AS builderCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm \ npm ciCOPY . .RUN --mount=type=cache,target=/root/.npm \ npm run build4.3 缓存导入/导出
在 CI/CD 中使用 --cache-from=type=registry 可以将上一轮构建的缓存作为起点,避免每次从头开始。对于 Go 项目,配合 --mount=type=cache,target=/go/pkg/mod 持久化模块缓存,构建时间可以从 5 分钟降到 30 秒以内。
# 导出缓存到 Registrydocker 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 .
# 导出缓存到 S3docker 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 多平台构建
# 创建多平台 builderdocker 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 updateRUN apt-get install -y nginxRUN apt-get install -y redisRUN 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 . /appRUN npm install
# 好:先复制 package.json,利用缓存COPY package.json package-lock.json /app/RUN npm installCOPY . /app5.3 安全优化
# 使用非 root 用户FROM alpine:3.19RUN adduser -D appuserUSER appuserCOPY --chown=appuser:appuser . /appWORKDIR /appCMD ["./myapp"]5.4 镜像大小优化对比
| 优化方式 | 镜像大小 | 说明 |
|---|---|---|
| 基础镜像 ubuntu:22.04 | 77.8MB | 未优化 |
| 基础镜像 alpine:3.19 | 7.3MB | 使用 Alpine |
| 多阶段构建 | ~15MB | 只复制二进制 |
| 静态编译 + scratch | ~5MB | 最小镜像 |
| distroless | ~2MB | 无 shell 的最小镜像 |
# 最小镜像:静态编译 + scratchFROM golang:1.22 AS builderWORKDIR /appCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp
FROM scratchCOPY --from=builder /app/myapp /myappENTRYPOINT ["/myapp"]# 最终镜像大小:~5MB(只包含静态链接的二进制)六、动手实践
6.1 构建优化实验
#!/bin/bash# 对比不同 Dockerfile 的构建时间和镜像大小
echo "=== 未优化的 Dockerfile ==="cat > Dockerfile.bad << 'EOF'FROM ubuntu:22.04RUN apt-get updateRUN apt-get install -y nginxRUN apt-get install -y curlCOPY . /appCMD ["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 baseFROM base AS builderCOPY --from=builder /etc/nginx/nginx.conf /etc/nginx/nginx.confCOPY . /usr/share/nginx/htmlEXPOSE 80CMD ["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. 启用 BuildKitexport 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 dudocker buildx prune七、CI/CD 中的镜像构建
7.1 GitHub Actions 构建示例
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/arm647.2 构建缓存策略
| 缓存策略 | 说明 | 适用场景 |
|---|---|---|
| inline cache | 缓存嵌入镜像层 | 简单场景 |
| registry cache | 缓存存储在 Registry | CI/CD 流水线 |
| local cache | 缓存存储在本地 | 本地开发 |
| GHA cache | GitHub 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:latest7.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.04RUN apt-get updateRUN apt-get install -y python3RUN apt-get install -y python3-pipRUN pip3 install flaskCOPY app.py /app/app.pyWORKDIR /appCMD ["python3", "app.py"]问题:
- 每条
RUN生成一个镜像层,5 条RUN= 5 层 apt-get update和apt-get install分开写,update 缓存留在镜像中- 使用
ubuntu:22.04(约 77MB)而非alpine(约 7MB) - 开发工具(pip、gcc)留在最终镜像中
附.2 优化后的 Dockerfile
# Dockerfile.good — 多阶段构建 + 层合并FROM python:3.12-alpine AS builderRUN pip install --no-cache-dir flaskFROM python:3.12-alpineCOPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packagesCOPY app.py /app/app.pyWORKDIR /appCMD ["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 | 排除无关文件 | 排除 .git、node_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 多架构 | 一次构建多平台镜像 |
高效的 Dockerfile 遵循三个原则:减少层数(合并 RUN 指令)、利用缓存(把不变的指令放前面)、多阶段构建(只复制运行时需要的文件)。BuildKit 的并行构建和缓存机制让这些优化更加有效。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






