首页 / Docker 入门教程 / 多阶段构建精简镜像

Docker 入门教程

多阶段构建精简镜像

本教程共 25 篇 · 第 14 篇 · 更新于 2026-07-26 · 约 8 分钟阅读

DockerDocker 入门教程多阶段构建镜像精简Dockerfilebuilder 模式最小镜像

14. 多阶段构建精简镜像

本节目标:搞懂多阶段构建的写法,学会把编译环境留在构建阶段、只把运行产物带进最终镜像,让镜像又小又安全。

上一章我们优化了构建速度,这一章优化镜像体积。

你可能遇到过这种事:构建一个 Java 或 Go 应用,镜像动不动七八百 MB。其实应用本身可能就十几 MB,剩下的全是编译器、构建工具、SDK—这些玩意儿只在构建时需要,运行时根本用不上,却全被塞进了镜像。

多阶段构建就是来解决这个问题的。它让你在一个 Dockerfile 里写多个「阶段」,构建阶段用大而全的镜像编译代码,最终阶段用小而精的镜像只装运行产物。两个阶段之间用 COPY --from 传递产物。

为什么需要多阶段构建

先看传统单阶段构建的问题。以一个 Spring Boot 应用为例,Dockerfile 大概长这样:

FROM eclipse-temurin:21.0.8_9-jdk-jammy
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY src ./src
CMD ["./mvnw", "spring-boot:run"]

构建出来多大?大约 880 MB。里面塞了完整的 JDK、Maven 工具链、所有构建依赖。但运行时只需要一个 JRE(Java Runtime Environment)和你那个 jar 包,根本用不着编译器。

这带来三个坏处:

  1. 拉取和分发慢,每次部署都要传几百 MB。
  2. 攻击面大,镜像里一堆构建工具,万一被入侵,攻击者手里工具齐全。
  3. 存储浪费,仓库、节点都要存这些大镜像。

多阶段构建能把这个镜像压到几百 MB 甚至几十 MB。

多阶段构建的语法

多阶段构建的核心是:一个 Dockerfile 里写多个 FROM,每个 FROM 开启一个新阶段。用 AS 给阶段起名,用 COPY --from=阶段名 把上一阶段的产物拷过来。

伪代码长这样:

# 阶段 1:构建环境
FROM builder-image AS build-stage
# 安装构建工具、拷源码、编译打包

# 阶段 2:运行环境
FROM runtime-image AS final-stage
# 从构建阶段拷贝产物,定义启动命令
COPY --from=build-stage /path/in/build /path/to/final
CMD ["..."]

关键点:

  • 每个 FROM 都是独立阶段,互不影响。
  • AS 给阶段命名,方便后面引用。
  • COPY --from=build-stage 从指定阶段拷文件,而不是从宿主机拷。
  • 最终镜像是最后一个阶段,前面的阶段只是「脚手架」,不会出现在最终镜像里。

一个完整示例:Java 应用

把前面的 Spring Boot 例子改成多阶段:

# 阶段 1:构建,用完整 JDK
FROM eclipse-temurin:21.0.8_9-jdk-jammy AS builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install

# 阶段 2:运行,只用精简 JRE
FROM eclipse-temurin:21.0.8_9-jre-jammy AS final
WORKDIR /opt/app
EXPOSE 8080
COPY --from=builder /opt/app/target/*.jar /opt/app/app.jar
ENTRYPOINT ["java", "-jar", "/opt/app/app.jar"]

拆开看:

  • 阶段 1(builder):用包含 JDK 的镜像,把源码拷进去,跑 mvn clean install 编译出 jar 包。这一阶段镜像很大,但没关系,它只是临时脚手架。
  • 阶段 2(final):换成只含 JRE 的精简镜像,从 builder 阶段拷出编译好的 jar 包,定义启动命令。

构建完看看体积对比:

$ docker images
REPOSITORY                TAG       SIZE
spring-app-multistage     latest    428MB
spring-app-singlestage    latest    880MB

从 880 MB 降到 428 MB,砍掉了一半多。砍掉的就是 JDK、Maven、源码和构建缓存。

Tip

想进一步精简,可以用 jlink 工具生成只含所需模块的最小 JRE,而不是用现成的 JRE 镜像。这对 Java 应用能再压缩一大截,官方 Eclipse Temurin 镜像文档有详细说明。

编译型语言最能受益

多阶段构建对编译型语言效果最明显,因为编译器和运行时是完全分开的。

Go 的例子

Go 编译出来的是个静态二进制,运行时啥都不需要。多阶段构建能让最终镜像只有十几 MB:

# 阶段 1:编译
FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

# 阶段 2:运行,用极简基础镜像
FROM gcr.io/distroless/static-debian12 AS final
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

最终阶段用的是 distroless(无发行版)镜像,里面连 shell 都没有,只有运行时必需的库,体积小、攻击面极小。

C / Rust 同理

C 和 Rust 编译出的二进制,如果静态链接,最终镜像甚至可以 FROM scratch—一个空镜像,只放你的二进制进去:

FROM rust:1.82 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM scratch AS final
COPY --from=builder /app/target/release/myapp /myapp
ENTRYPOINT ["/myapp"]

scratch 是真正的「空」,镜像里只有你拷进去的那个二进制,体积等于二进制本身的大小,通常几 MB。

Warning

scratch 时要注意:里面没有 shell、没有 ls、没有 cat,调试很不方便。而且如果二进制是动态链接的,依赖的 .so 文件也得一起拷进去,否则跑不起来。静态编译是前提。

解释型语言也能用

解释型语言(Python、Node、Ruby)没有「编译」这一步,但多阶段构建依然有用。

典型场景是:构建阶段做转译、压缩、装全量依赖;运行阶段只带生产依赖和转译后的产物。

# 阶段 1:构建,装全部依赖并打包
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build

# 阶段 2:运行,只装生产依赖,拷构建产物
FROM node:22-alpine AS final
WORKDIR /app
ENV NODE_ENV=production
COPY package.json yarn.lock ./
RUN yarn install --production --frozen-lockfile
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

这样最终镜像里不会有 devDependencies、不会有源码、不会有 TypeScript 编译器,只有运行需要的东西。

只构建指定阶段

有时候你想单独测试构建阶段,不想跑完整流程。用 --target 指定构建到哪个阶段为止:

docker build --target builder -t myapp-builder .

这条命令只构建到 builder 阶段就停,产出的镜像里有完整的构建环境和编译产物,方便你进去排查编译问题。

不加 --target 时,Docker 默认构建最后一个阶段。

多阶段构建的好处再强调一遍

总结一下为什么多阶段构建是官方推荐的写法:

  1. 镜像更小:只带运行必需的东西,分发快、存储省。
  2. 更安全:构建工具、源码、密钥都不会进最终镜像,攻击面大幅收窄。
  3. 构建缓存友好:构建阶段改代码不影响最终阶段的拉取,CI 里推送和拉取都更快。
  4. 一个 Dockerfile 搞定:不用再写两个 Dockerfile 加一个脚本去拼,所有逻辑集中在一处,可维护。
Note

多阶段构建依赖 BuildKit。Docker Engine 29.x 默认启用 BuildKit,开箱即用。如果你看到构建日志是老式的 Step 1/10 : 而不是 [+] Building 的树状输出,说明没启用 BuildKit,多阶段构建虽然能用,但并行和缓存优化会打折扣。

下一章我们讲怎么把构建好的镜像推到 Docker Hub 和国内镜像仓库,让镜像能在任何机器上跑起来。