在 Docker 中,如何构建多阶段镜像以减少镜像体积?
在 Docker 中,多阶段构建(Multi-Stage Builds)是减少镜像体积、提高安全性的最有效技术。它允许你在一个Dockerfile中使用多个FROM语句,将构建环境(如编译器、SDK、构建工具)与最终运行环境分离,只将必要的产物复制到最终镜像中。
一、核心原理
传统的 Docker 构建方式会将所有构建步骤产生的文件都保留在最终镜像中,导致镜像体积巨大(例如包含gcc,make,maven,node_modules等)。
多阶段构建的工作流程:
- 构建阶段(Builder Stage):使用包含完整开发工具链的基础镜像(如
golang:1.21,maven:3.8,node:18),编译代码、安装依赖。 - 运行阶段(Final Stage):使用极简的基础镜像(如
alpine,distroless,scratch),仅从构建阶段复制编译好的二进制文件或静态资源。 - 结果:最终镜像不包含任何构建工具、源代码或中间文件,体积极小。
二、基础语法
在Dockerfile中,通过给FROM指令指定AS别名来定义阶段:
# 阶段 1:构建 FROM <构建基础镜像> AS builder # ... 构建命令 ... # 阶段 2:运行 FROM <运行基础镜像> # 从阶段 1 复制文件 COPY --from=builder <源路径> <目标路径> # ... 运行命令 ...三、实战案例
案例 1:Go 语言应用(从 800MB+ 减少到 10MB+)
传统方式(体积大):
FROM golang:1.21 WORKDIR /app COPY . . RUN go build -o main . CMD ["./main"] # 镜像包含 Go 编译器、源码、构建缓存,体积约 800MB+多阶段构建(体积极小):
# 阶段 1:构建 FROM golang:1.21-alpine AS builder WORKDIR /app # 安装 git 等构建依赖(如果需要拉取依赖) RUN apk add --no-cache git COPY . . # 编译静态二进制文件 RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # 阶段 2:运行 FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ # 仅从 builder 阶段复制编译好的二进制文件 COPY --from=builder /app/main . # 暴露端口 EXPOSE 8080 CMD ["./main"]- 效果:最终镜像仅包含
alpine基础库和二进制文件,体积通常< 15MB。 - 关键点:
CGO_ENABLED=0确保生成静态链接的二进制文件,无需在运行镜像中安装 glibc。
案例 2:Java (Maven/Gradle) 应用
Java 应用通常构建慢且依赖多,多阶段构建可以分离构建环境和运行环境。
# 阶段 1:构建 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . # 下载依赖(利用 Docker 缓存层) RUN mvn dependency:go-offline -B COPY src ./src # 打包 RUN mvn package -DskipTests # 阶段 2:运行 FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 仅复制 jar 包 COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]- 效果:最终镜像只包含 JRE 和 Jar 包,去除了 Maven、源码和构建缓存。
案例 3:Node.js 前端应用(React/Vue)
前端构建需要node_modules和构建工具,但运行只需要静态文件。
# 阶段 1:构建 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . # 构建生产版本 RUN npm run build # 阶段 2:运行 (使用 Nginx 托管静态文件) FROM nginx:alpine # 复制构建产物到 Nginx 目录 COPY --from=builder /app/dist /usr/share/nginx/html # 可选:复制自定义 nginx 配置 COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]- 效果:最终镜像只有 Nginx 和静态 HTML/CSS/JS,没有
node_modules、npm或源代码。
案例 4:Python 应用
# 阶段 1:构建 FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . # 安装依赖到临时目录 RUN pip install --user --no-cache-dir -r requirements.txt # 阶段 2:运行 FROM python:3.11-slim WORKDIR /app # 复制用户安装的包 COPY --from=builder /root/.local /root/.local # 确保路径包含 ENV PATH=/root/.local/bin:$PATH COPY . . CMD ["python", "main.py"]四、高级技巧
1. 使用distroless镜像(极致安全与体积)
Google 的distroless镜像只包含运行应用所需的文件和依赖,没有 shell、包管理器等。
FROM golang:1.21 AS builder # ... 构建 ... # 使用 distroless 作为运行镜像 FROM gcr.io/distroless/static-debian11 COPY --from=builder /app/main . CMD ["./main"]- 优势:体积极小(< 5MB),攻击面极小(无法在容器内执行 shell 命令)。
2. 缓存优化
利用 Docker 层缓存机制,将不常变的依赖下载放在COPY代码之前:
FROM maven:3.9 AS builder COPY pom.xml . RUN mvn dependency:go-offline -B # 这一层会被缓存,除非 pom.xml 变化 COPY src ./src RUN mvn package -DskipTests3. 多架构构建
结合docker buildx构建多架构镜像:
dockerbuildx build--platformlinux/amd64,linux/arm64-tmyapp:latest--push.4. 条件构建
根据构建参数选择不同阶段(较少用,但可行):
ARG BUILD_ENV=prod FROM node:18 AS dev # ... FROM node:18 AS prod # ... FROM ${BUILD_ENV} AS final # ...五、对比总结
| 特性 | 传统单阶段构建 | 多阶段构建 |
|---|---|---|
| 镜像体积 | 大(包含构建工具、源码) | 极小(仅含运行所需) |
| 安全性 | 低(包含 shell、编译器,易被攻击) | 高(无多余工具,攻击面小) |
| 构建速度 | 较慢(每次全量构建) | 快(利用缓存层) |
| 维护性 | 低(Dockerfile 臃肿) | 高(逻辑清晰,阶段分离) |
| 依赖管理 | 混乱 | 清晰(构建依赖与运行依赖分离) |
六、最佳实践建议
- 始终使用多阶段构建:这是现代 Docker 构建的标准做法。
- 选择最小的运行镜像:优先使用
alpine、slim或distroless。 - 静态链接:对于 Go、Rust 等语言,尽量编译静态二进制文件,避免依赖运行环境的库。
- 清理缓存:在构建阶段使用
RUN ... && rm -rf ...清理临时文件(虽然多阶段构建通常不需要,但在单阶段中很重要)。 - 利用
.dockerignore:排除不必要的文件(如.git,node_modules,*.log),减少构建上下文大小。 - 不要复制整个目录:
COPY --from=builder /app/dist /app比COPY --from=builder /app /app更安全,避免意外复制源码。
