了解Docker的多阶段构建(Multi-stage Build)
概述
你有没有发现,自己构建的 Docker 镜像动不动就几百 MB,甚至上 GB?
而别人分享的镜像,功能一样,却只有几十 MB?
问题很可能出在:你把“构建环境”也打包进了最终镜像。
比如:
- 用 Node.js 构建前端,却把
node_modules
和npm
工具留在了生产镜像 - 用 Maven 编译 Java,却把整个 JDK 打包进去
- 用 Python 编译依赖,却包含了
pip
、编译器等构建工具
这些在运行时根本不需要的文件,白白占用了大量空间,还带来了安全风险。
解决方案就是:多阶段构建(Multi-stage Build)
什么是多阶段构建
多阶段构建允许你在一个 Dockerfile
中定义多个构建阶段,每个阶段可以基于不同的基础镜像。
你可以:
- 在第一阶段:使用“胖镜像”完成编译、打包
- 在第二阶段:使用“瘦镜像”只复制必要的产物(如可执行文件、静态文件)
最终镜像只包含运行所需的内容,不包含构建工具和中间文件。
用多阶段构建优化 Node.js 应用
场景
你有一个 React 前端项目,需要:
- 安装依赖(
npm install
) - 打包构建(
npm run build
) - 用 Nginx 托管静态文件
普通构建(镜像臃肿)
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:alpine
COPY --from=0 /app/build /usr/share/nginx/html
EXPOSE 80
多阶段构建(镜像精简)
# 阶段 1:构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build# 阶段 2:运行阶段
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
效果对比
方式 | 镜像大小 | 是否包含 Node.js? |
---|---|---|
普通构建 | ~300MB | 是(不需要) |
多阶段构建 | ~20MB | 否 |
体积减少 93%
多阶段构建如何工作
-
第一阶段(
builder
):- 使用
node:18-alpine
,安装所有依赖并执行npm run build
- 生成
build/
目录(静态文件)
- 使用
-
第二阶段(运行):
- 使用极轻的
nginx:alpine
(仅 ~16MB) - 只复制
builder
阶段生成的build/
文件 - 最终镜像不包含 Node.js、npm、源码等
- 使用极轻的
关键语法:COPY --from=builder ...
,表示从名为 builder
的阶段复制文件
更多场景
- Java(Spring Boot)项目
# 阶段 1:编译
FROM maven:3.8-openjdk-17 AS builder
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests# 阶段 2:运行
FROM eclipse-temurin:17-jre-alpine
COPY --from=builder /target/myapp.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
优势:最终镜像使用 JRE 而非 JDK,体积更小
- Golang 应用
# 阶段 1:编译
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o main .# 阶段 2:运行(使用极小的 distroless 镜像)
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/main .
CMD ["/main"]
优势:最终镜像不含 shell、包管理器,极致轻量且安全
多阶段构建的好处
优势 | 说明 |
---|---|
镜像体积大幅减小 | 只包含运行时所需文件 |
构建更高效 | 可复用构建阶段 |
安全性更高 | 不包含编译器、shell 等攻击面 |
职责分离 | 构建与运行环境解耦 |
最佳实践
- 为阶段命名:使用
AS <name>
,便于引用 - 选择合适的运行基础镜像:
- 前端:
nginx:alpine
- Java:
eclipse-temurin:17-jre-alpine
- Go:
distroless
或scratch
- 前端:
- 避免在最终阶段安装不必要的包
- 在 CI/CD 中使用多阶段构建,确保一致性
常见问题
-
多阶段构建会影响构建速度吗?
不会。虽然阶段多,但 Docker 会缓存每一层,构建速度通常更快。 -
如何只构建某个阶段?
docker build --target builder -t myapp:build .
这在调试时非常有用。
总结
传统方式 | 多阶段构建 |
---|---|
一个镜像搞定所有 | 分阶段,各司其职 |
镜像臃肿 | 镜像精简 |
安全性低 | 安全性高 |
多阶段构建是编写高质量 Dockerfile 的必备技能,尤其适合生产环境