Dockerfile 多阶段构建
概述
单阶段构建
Docker V17.05 之前,Dockerfile 中只能有一个 FROM 指令,称为单阶段构建。
在单阶段构建中,所有的操作(编译、打包、配置等)都在同一个镜像中完成,导致最终镜像体积较大,且可能包含不必要的构建工具。
多阶段构建
Docker V17.05 之后,新增多阶段构建(multistage builds)。
Dockerfile 中允许有多个 FROM 指令,每个 FROM 指令代表一个阶段,称为多阶段构建。
核心优势
- 减小镜像体积
- 只保留最终运行所需的文件
- 构建工具和依赖不会出现在最终镜像中
- 提高安全性
- 简化构建流程
- 可以在单个 Dockerfile 中完成所有构建步骤
- 无需编写多个 Dockerfile 或使用外部脚本
基本原理
工作流程
多阶段构建的工作原理如下:
- 多个 FROM 指令:每个 FROM 指令开始一个新的构建阶段
- 阶段命名:使用
AS 关键字为阶段命名
- 跨阶段复制:使用
COPY --from=<stage-name> 从前一阶段复制文件
- 最终镜像:最后一个 FROM 指令生成的镜像为最终镜像
阶段结构
| Docker |
|---|
| # 阶段1:构建阶段
FROM <构建工具镜像> AS <阶段名>
... 构建操作 ...
# 阶段2:运行阶段
FROM <运行时镜像> AS <阶段名>
COPY --from=<阶段名> <源路径> <目标路径>
... 运行时配置 ...
|
实战示例
需求场景
典型的 Java 应用构建流程:
| Text Only |
|---|
| 下载源代码 → Maven 编译 → 生成 .jar 文件 → 部署到 Java 运行环境 → 打包镜像
|
多阶段构建实现
方法1:明确指定构建阶段
| Docker |
|---|
| # 阶段1:Maven 构建阶段
FROM maven:3.8.3 AS maven-build
WORKDIR /app
COPY code .
RUN mvn -f pom.xml clean package -DskipTests=true
# 阶段2:运行阶段
FROM openjdk:8-alpine
WORKDIR /app
COPY --from=maven-build /app/code/target/myapp.jar /app
EXPOSE 8080
CMD ["java", "-jar", "/app/myapp.jar"]
|
逐行解析
阶段1:Maven 构建
| Docker |
|---|
| FROM maven:3.8.3 AS maven-build
|
- 使用 Maven 3.8.3 镜像作为基础
- 将此阶段命名为
maven-build
- Maven 镜像包含 Java 和 Maven 构建工具,体积较大
| Docker |
|---|
| RUN mvn -f pom.xml clean package -DskipTests=true
|
- 使用 Maven 编译代码
- 跳过测试(构建时)
- 生成的
.jar 文件在 /app/code/target/ 目录
阶段2:运行时
- 使用 OpenJDK 8 Alpine 版本作为基础镜像
- Alpine 版本体积小,适合运行时环境
| Docker |
|---|
| WORKDIR /app
COPY --from=maven-build /app/code/target/myapp.jar /app
|
- 从
maven-build 阶段复制编译好的 .jar 文件
- Maven 工具和源代码都不会被复制到这个阶段
- 最终镜像只包含 JRE 和应用 JAR
| Docker |
|---|
| EXPOSE 8080
CMD ["java", "-jar", "/app/myapp.jar"]
|
构建和运行
| Bash |
|---|
| # 构建镜像
docker build -t my-java-app .
# 运行容器
docker run -d -p 8080:8080 --name myapp my-java-app
|
指定构建阶段
在多阶段构建中,可以只构建到某个特定阶段:
| Bash |
|---|
| # 主要测试阶段一是否成功
docker build --target maven-build -t build03 .
|
用途:
- 调试构建流程
- 检查中间产物
- 分别构建不同阶段
更多实例
1. Go 应用
| Docker |
|---|
| # 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp
# 运行阶段
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
|
2. Python 应用
| Docker |
|---|
| # 构建阶段
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# 运行阶段
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "main.py"]
|
3. Node.js 应用
| Docker |
|---|
| # 构建阶段
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 运行阶段
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --production
CMD ["node", "dist/index.js"]
|
4. Rust 应用
| Docker |
|---|
| # 构建阶段
FROM rust:1.75 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
# 运行阶段
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /app/target/release/myapp .
CMD ["./myapp"]
|
高级技巧
1. 使用外部镜像作为阶段
| Docker |
|---|
| FROM alpine AS dependency
RUN apk add --no-cache curl
FROM golang:1.21 AS builder
COPY --from=dependency /lib /lib
# ... 构建代码
|
2. 嵌套引用
| Docker |
|---|
| FROM ubuntu AS base
RUN apt-get update && apt-get install -y git
FROM base AS build
RUN git clone https://github.com/some/repo.git
FROM base AS deploy
COPY --from=build /repo /app
|
3. 在构建时选择阶段
| Bash |
|---|
| # 构建 builder 阶段
docker build --target builder -t myapp:builder .
# 构建 deploy 阶段
docker build --target deploy -t myapp:deploy .
|
4. 复用构建缓存
| Docker |
|---|
| FROM golang:1.21 AS deps
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
FROM golang:1.21 AS builder
WORKDIR /app
COPY --from=deps /go/pkg /go/pkg
COPY . .
RUN go build
|
最佳实践
1. 选择合适的基础镜像
- 构建阶段:使用包含完整工具链的镜像(如
maven:3.8.3)
- 运行阶段:使用精简版本(如
openjdk:8-alpine、python:3.11-slim)
2. 命名清晰的阶段名称
| Docker |
|---|
| FROM golang:1.21 AS build-stage # 而不是 stage1
FROM alpine:latest AS run-stage # 而不是 stage2
|
3. 只复制必要的文件
| Docker |
|---|
| # 不推荐:复制整个目录
COPY --from=builder /app /app
# 推荐:只复制需要的文件
COPY --from=builder /app/target/myapp /app/
|
4. 清理构建产物
在构建阶段,及时清理不必要的文件:
| Docker |
|---|
| RUN go build -o myapp && \
rm -rf /go/pkg /go/src /go/bin
|
5. 使用构建参数(ARG)
| Docker |
|---|
| ARG BUILD_VERSION=1.0
FROM golang:1.21 AS builder
ARG BUILD_VERSION
RUN echo "Building version: ${BUILD_VERSION}"
|
性能优化
1. 利用层缓存
将变化较少的指令放在前面:
| Docker |
|---|
| FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./ # 先复制依赖文件
RUN go mod download # 下载依赖
COPY . . # 再复制代码
RUN go build # 构建应用
|
2. 使用多阶段构建的 --from=0
如果不需要为阶段命名,可以使用索引引用:
| Docker |
|---|
| FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM alpine
COPY --from=0 /app/myapp .
|
与单阶段构建对比
| 特性 |
单阶段构建 |
多阶段构建 |
| 镜像体积 |
大(包含构建工具) |
小(只包含运行时) |
| 构建速度 |
快(一次构建) |
慢(多次构建) |
| 安全性 |
低(可能泄露源代码) |
高(只包含必要文件) |
| 灵活性 |
低(难以复用中间产物) |
高(可以单独构建任意阶段) |
| 维护性 |
简单 |
需要理解阶段概念 |
适用场景
推荐使用多阶段构建
- 需要编译型语言(C/C++、Go、Rust、Java 等)
- 对镜像体积有严格要求
- 需要保证最终镜像安全性
- 项目较大,构建工具占用空间多
可以使用单阶段构建
- 解释型语言(Python、Node.js、PHP 等)
- 项目较小,镜像体积不是问题
- 快速原型开发
总结
多阶段构建是 Docker 提供的强大功能,通过将构建过程分为多个阶段,可以:
- 显著减小镜像体积:只保留运行时所需文件
- 提高安全性:避免源代码和构建工具泄露
- 简化构建流程:单个 Dockerfile 完成所有操作
掌握多阶段构建,能够帮助你构建更高效、更安全的 Docker 镜像。对于生产环境,强烈推荐使用多阶段构建来优化镜像。