当你执行 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。
前置知识
Ch05 OCI 规范详解:镜像构建的产出是 OCI Image,理解 Image Spec 是理解构建过程的前提
Ch04 OverlayFS:容器文件系统:镜像层通过 OverlayFS 实现复用,分层存储是构建缓存的基础
Dockerfile 基础:
FROM、RUN、COPY、CMD等基本指令
本章假设你已写过基本的 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: 安装 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(Low-Level Build)中间表示来表达构建依赖关系。与旧版 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.17WORKDIR /appCOPY . .RUN go build -o myappCMD ["./myapp"]# 最终镜像大小:~800MB3.2 多阶段构建
# 阶段 1:构建FROM golang:1.17 AS builderWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o myapp
# 阶段 2:运行FROM alpine:3.14RUN apk --no-cache add ca-certificatesCOPY --from=builder /app/myapp /usr/local/bin/myappEXPOSE 8080CMD ["myapp"]# 最终镜像大小:~15MB(只包含二进制 + Alpine)3.3 多阶段构建的优化
BuildKit 只构建被最终阶段引用的阶段:
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 /src4.2 BuildKit 增量缓存实战
BuildKit 的 --mount=type=cache 可以在构建之间持久化包管理器缓存,避免每次重新下载依赖:
# Go 模块缓存持久化FROM golang:1.17 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.14RUN adduser -D appuserUSER appuserCOPY --chown=appuser:appuser . /appWORKDIR /appCMD ["./myapp"]5.4 镜像大小优化对比
| 优化方式 | 镜像大小 | 说明 |
|---|---|---|
| 基础镜像 ubuntu<22>22>.04 | 77.8MB | 未优化 |
| 基础镜像 alpine<3>3>.14 | 7.3MB | 使用 Alpine |
| 多阶段构建 | ~15MB | 只复制二进制 |
| 静态编译 + scratch | ~5MB | 最小镜像 |
| distroless | ~2MB | 无 shell 的最小镜像 |
# 最小镜像:静态编译 + scratchFROM golang:1.17 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 node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ci --productionCOPY . .RUN npm run build
# 第二阶段:只把构建产物放进运行镜像FROM nginx:alpineCOPY --from=builder /app/dist /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}}"# builder 阶段的 node_modules、npm 等都不会进入最终镜像,体积远小于 bad 版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 多架构 | 一次构建多平台镜像 |
减少层数、利用缓存、多阶段构建,三条原则背后是同一个目标:让构建可复现、让镜像可预测。合并 RUN 指令不仅减少层数,更重要的是消除中间状态的缓存污染(apt-get update 和 apt-get install 分开写就会遇到这个问题)。把不变指令放前面,本质上是把”变的部分”和”不变的部分”解耦,代码每天都在改,但依赖很少变。多阶段构建则把”构建时需要的”和”运行时需要的”严格分开,让最终镜像只包含运行必需的文件。
参考资料
- Dockerfile reference - Docker 官方 Dockerfile 语法参考
- BuildKit 官方仓库 - BuildKit 架构设计与使用指南
- Docker 最佳实践 - Docker 官方镜像构建最佳实践
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






