4. 多阶段构建概念:语法、阶段命名与文件复制
多阶段构建(Multi-stage Build)是 Docker 17.05 引入的核心特性,它允许在单个 Dockerfile 中使用多个 FROM 语句,每个 FROM 定义一个独立的构建阶段。前一阶段的产物(如编译后的二进制文件、依赖包)可以通过 COPY --from 精确复制到后续阶段,而前一阶段的基础镜像、临时文件、构建工具链则被完全丢弃。这一机制从根本上解决了传统单阶段构建中镜像体积膨胀的问题。
4.1 多阶段构建的语法结构
多阶段构建的语法极其简洁,核心只有三个关键词:FROM、AS、COPY --from。下面是一个典型的 Go 语言后端服务多阶段构建 Dockerfile 示例:
# 第一阶段:编译阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server
# 第二阶段:运行阶段
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/server ./server
EXPOSE 8080
CMD ["./server"]
上述 Dockerfile 清晰地展示了多阶段构建的三个基本要素:
| 语法要素 | 作用 | 示例 |
|---|---|---|
| 多个 FROM 语句 | 每个 FROM 开启一个新的构建阶段,可基于不同的基础镜像 | FROM golang:1.21-alpine AS builderFROM alpine:3.19 |
| 阶段命名(AS) | 为当前阶段指定一个逻辑名称,供后续阶段引用 | AS builder |
| 阶段间文件复制(COPY --from) | 从指定阶段复制文件到当前阶段,不依赖镜像层缓存 | COPY --from=builder /app/server ./server |
4.2 阶段命名(AS)的引用机制
AS 关键字为阶段赋予一个字符串别名,后续阶段可以通过该别名引用前一阶段的产物。阶段名称区分大小写,建议统一使用小写加下划线或驼峰命名。除了通过名称引用,Docker 还支持通过阶段索引(从 0 开始)引用:
# 通过阶段名称引用(推荐)
COPY --from=builder /app/server ./server
# 通过阶段索引引用(不推荐,维护性差)
COPY --from=0 /app/server ./server
阶段索引的引用方式在 Dockerfile 调整阶段顺序时容易出错,因此生产环境中应始终使用 AS 命名并引用名称。阶段名称的作用域是整个 Dockerfile,即使某个阶段在后续未被引用,其名称依然有效。
4.3 阶段间文件复制(COPY --from)的细节
COPY --from 是多阶段构建的核心操作,它允许你从任意一个已完成的阶段中复制文件或目录到当前阶段。其语法与普通 COPY 指令一致,但多了 --from 参数:
COPY --from=<阶段名称或索引> <源路径> <目标路径>
关键行为规则:
- 源路径:相对于前一阶段的工作目录(
WORKDIR),也可以是绝对路径。 - 目标路径:相对于当前阶段的工作目录,或绝对路径。
- 文件权限:复制过来的文件保留原始权限(包括可执行权限),无需额外
RUN chmod。 - 符号链接:默认复制符号链接指向的实际文件,而非链接本身(与普通 COPY 行为一致)。
- 多文件复制:支持通配符,如
COPY --from=builder /app/dist/*.js ./。
COPY --from 指定的阶段名称不存在或拼写错误,Docker 会在构建时报错 failed to copy: stage not found。另外,COPY --from 只能复制已完成阶段的文件系统内容,不能复制未执行指令的中间状态。
4.4 多阶段构建的流程示意图
下图展示了多阶段构建的完整数据流:第一阶段(builder)完成编译和依赖安装,第二阶段(runtime)只接收最终产物,中间层(SDK、临时文件、缓存)全部被丢弃。
4.5 多阶段构建 vs 传统单阶段构建
下表从镜像体积、构建速度、安全性、维护性四个维度对比两种构建模式:
| 对比维度 | 传统单阶段构建 | 多阶段构建 |
|---|---|---|
| 镜像体积 | 包含 SDK、编译工具、临时文件,体积通常 300MB~1GB+ | 仅包含运行时二进制和必要依赖,体积可压缩至 10~50MB |
| 构建速度 | 依赖层缓存,但每次修改源码需重新下载依赖(若层设计不佳) | 依赖分层缓存,且编译阶段可独立缓存依赖层,增量构建更快 |
| 安全性 | 镜像中残留编译器、调试工具、源码,攻击面大 | 运行镜像极简,无 shell、无编译器,攻击面显著缩小 |
| 维护性 | 需手动清理临时文件,Dockerfile 复杂且易出错 | Dockerfile 结构清晰,阶段职责单一,易于理解和维护 |
4.6 多阶段构建的扩展用法
除了标准的编译+运行两阶段模式,多阶段构建还支持更复杂的拓扑:
- 三阶段模式:依赖安装阶段 → 编译阶段 → 运行阶段,将依赖下载与编译分离,进一步提升缓存命中率。
- 测试阶段:在编译阶段之后插入一个测试阶段,运行单元测试或 lint,测试通过后再进入运行阶段。
- 多产物输出:一个 Dockerfile 中定义多个运行阶段(如 API 服务、Worker 服务),通过
--target参数选择构建哪个阶段。
# 多目标构建示例:通过 --target 选择构建产物
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS build
COPY . .
RUN npm run build
FROM base AS development
RUN npm install --only=dev
CMD ["npm", "run", "dev"]
FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
构建时通过 docker build --target production -t my-app:latest . 指定目标阶段,实现一个 Dockerfile 同时支持开发、测试、生产三种场景。