跳转至

Dockerfile 多阶段构建

概述

单阶段构建

Docker V17.05 之前,Dockerfile 中只能有一个 FROM 指令,称为单阶段构建。

在单阶段构建中,所有的操作(编译、打包、配置等)都在同一个镜像中完成,导致最终镜像体积较大,且可能包含不必要的构建工具。

多阶段构建

Docker V17.05 之后,新增多阶段构建(multistage builds)。

Dockerfile 中允许有多个 FROM 指令,每个 FROM 指令代表一个阶段,称为多阶段构建。

核心优势

  1. 减小镜像体积
  • 只保留最终运行所需的文件
  • 构建工具和依赖不会出现在最终镜像中
  1. 提高安全性
  • 源代码和构建临时文件不会泄露到最终镜像
  1. 简化构建流程
  • 可以在单个 Dockerfile 中完成所有构建步骤
  • 无需编写多个 Dockerfile 或使用外部脚本

基本原理

工作流程

多阶段构建的工作原理如下:

  1. 多个 FROM 指令:每个 FROM 指令开始一个新的构建阶段
  2. 阶段命名:使用 AS 关键字为阶段命名
  3. 跨阶段复制:使用 COPY --from=<stage-name> 从前一阶段复制文件
  4. 最终镜像:最后一个 FROM 指令生成的镜像为最终镜像

阶段结构

Docker
1
2
3
4
5
6
7
8
# 阶段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
WORKDIR /app
COPY code .
  • 设置工作目录为 /app
  • 复制源代码到镜像中
Docker
RUN mvn -f pom.xml clean package -DskipTests=true
  • 使用 Maven 编译代码
  • 跳过测试(构建时)
  • 生成的 .jar 文件在 /app/code/target/ 目录

阶段2:运行时

Docker
FROM openjdk:8-alpine
  • 使用 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"]
  • 声明端口 8080
  • 设置容器启动命令

构建和运行

Bash
1
2
3
4
5
# 构建镜像
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
1
2
3
4
5
6
FROM alpine AS dependency
RUN apk add --no-cache curl

FROM golang:1.21 AS builder
COPY --from=dependency /lib /lib
# ... 构建代码

2. 嵌套引用

Docker
1
2
3
4
5
6
7
8
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
1
2
3
4
5
# 构建 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-alpinepython:3.11-slim

2. 命名清晰的阶段名称

Docker
FROM golang:1.21 AS build-stage  # 而不是 stage1
FROM alpine:latest AS run-stage   # 而不是 stage2

3. 只复制必要的文件

Docker
1
2
3
4
5
# 不推荐:复制整个目录
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
1
2
3
4
ARG BUILD_VERSION=1.0
FROM golang:1.21 AS builder
ARG BUILD_VERSION
RUN echo "Building version: ${BUILD_VERSION}"

性能优化

1. 利用层缓存

将变化较少的指令放在前面:

Docker
1
2
3
4
5
6
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
1
2
3
4
5
6
7
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 提供的强大功能,通过将构建过程分为多个阶段,可以:

  1. 显著减小镜像体积:只保留运行时所需文件
  2. 提高安全性:避免源代码和构建工具泄露
  3. 简化构建流程:单个 Dockerfile 完成所有操作

掌握多阶段构建,能够帮助你构建更高效、更安全的 Docker 镜像。对于生产环境,强烈推荐使用多阶段构建来优化镜像。