一文搞懂 Docker Multi-stage Build:镜像从 1.2GB 瘦到 45MB
Understanding Docker Multi-stage Build: Shrinking Images from 1.2GB to 45MB
| David Wang | 2026-08-03T20:35:07
Java 项目的 Docker 镜像动不动就 1GB+,严重拖慢部署速度。这篇文章手把手教你用多阶段构建把镜像压到极致。
A step-by-step guide to using Docker multi-stage builds to dramatically reduce Java application image sizes.
## 问题:1.2GB 的镜像 我们的 Spring Boot 项目,Dockerfile 长这样: ```dockerfile FROM openjdk:17 COPY target/app.jar /app.jar EXPOSE 8080 CMD ["java", "-jar", "/app.jar"] ``` `docker images` 一看:**1.2GB**。每次部署拉镜像就要好几分钟,CI/CD 流水线慢得让人崩溃。 1.2GB 里到底都有啥? ``` openjdk:17 基础镜像 ~680MB(包含了完整的 Debian + JDK) app.jar ~85MB Maven 缓存等杂七杂八 ~400MB ``` ## 第一步:换基础镜像 `openjdk:17` 基于完整的 Debian,包含了一堆我们用不到的东西(gcc、make、man pages...)。换成 Alpine 版本或者 Eclipse Temurin 的 JRE: ```dockerfile FROM eclipse-temurin:17-jre-alpine COPY target/app.jar /app.jar EXPOSE 8080 CMD ["java", "-jar", "/app.jar"] ``` 镜像大小:**1.2GB → 280MB**。仅仅换个基础镜像就砍掉了 75%。 ## 第二步:多阶段构建 上面的方式要求先在本地 `mvn package`,再 `docker build`。但在 CI 环境里,我们希望 Docker 自己完成编译。这就是多阶段构建的用途: ```dockerfile # 阶段一:编译 FROM eclipse-temurin:17-jdk-alpine AS builder WORKDIR /build COPY pom.xml . # 先下载依赖(利用 Docker 层缓存) RUN apk add --no-cache maven && mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 阶段二:运行 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 CMD ["java", "-jar", "app.jar"] ``` 关键点: - `AS builder` 给编译阶段命名 - `COPY --from=builder` 只从编译阶段复制 jar,JDK、Maven、源代码都不会出现在最终镜像里 - `mvn dependency:go-offline` 单独一层,依赖没变时直接走缓存 镜像大小:**280MB → 195MB**。 ## 第三步:jlink 自定义 JRE 完整的 JRE 包含了很多用不到的模块(CORBA、Swing、AWT...)。用 `jlink` 可以构建只包含必要模块的最小 JRE: ```dockerfile # 阶段一:编译 FROM eclipse-temurin:17-jdk-alpine AS builder WORKDIR /build COPY pom.xml . RUN apk add --no-cache maven && mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 阶段二:分析依赖并构建自定义 JRE FROM eclipse-temurin:17-jdk-alpine AS jre-builder COPY --from=builder /build/target/*.jar /app.jar RUN jdeps --ignore-missing-deps --print-module-deps /app.jar > /modules.txt RUN jlink --add-modules $(cat /modules.txt) --strip-debug --no-man-pages --no-header-files --compress=2 --output /custom-jre # 阶段三:最终运行镜像 FROM alpine:3.19 COPY --from=jre-builder /custom-jre /opt/java COPY --from=builder /build/target/*.jar /app/app.jar ENV PATH="/opt/java/bin:$PATH" EXPOSE 8080 CMD ["java", "-jar", "/app/app.jar"] ``` 三个阶段:编译 → 构建最小 JRE → 组装运行镜像。 镜像大小:**195MB → 75MB**。 ## 第四步:Spring Boot 分层 jar Spring Boot 2.3+ 支持分层 jar,可以把依赖和应用代码分开。这样改代码时只需要重新构建应用层,依赖层走缓存: ```dockerfile # 阶段一:编译 FROM eclipse-temurin:17-jdk-alpine AS builder WORKDIR /build COPY pom.xml . RUN apk add --no-cache maven && mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 解包分层 jar RUN java -Djarmode=layertools -jar target/*.jar extract --destination /extracted # 阶段二:自定义 JRE(同上,省略) # 阶段三:分层组装 FROM alpine:3.19 COPY --from=jre-builder /custom-jre /opt/java ENV PATH="/opt/java/bin:$PATH" WORKDIR /app # 按变化频率从低到高排列(利用层缓存) COPY --from=builder /extracted/dependencies/ ./ COPY --from=builder /extracted/spring-boot-loader/ ./ COPY --from=builder /extracted/snapshot-dependencies/ ./ COPY --from=builder /extracted/application/ ./ EXPOSE 8080 CMD ["java", "org.springframework.boot.loader.launch.JarLauncher"] ``` 镜像大小不变,但**增量构建速度提升 80%**——改业务代码只需要重新构建最后一层(通常只有几 MB)。 ## 第五步:GraalVM Native Image(终极) 如果追求极致,可以用 GraalVM 编译成原生镜像: ```dockerfile FROM ghcr.io/graalvm/native-image:17 AS builder WORKDIR /build COPY . . RUN ./mvnw -Pnative native:compile FROM alpine:3.19 COPY --from=builder /build/target/app /app EXPOSE 8080 CMD ["/app"] ``` 镜像大小:**75MB → 45MB**,启动时间从 3 秒降到 0.1 秒。但编译时间很长(5-10 分钟),而且反射等动态特性需要额外配置。 ## 最终对比 | 阶段 | 镜像大小 | 优化手段 | |------|---------|---------| | 原始 | 1.2GB | 无 | | 换基础镜像 | 280MB | alpine + JRE | | 多阶段构建 | 195MB | 编译和运行分离 | | jlink | 75MB | 自定义最小 JRE | | Native Image | 45MB | GraalVM AOT | 从 1.2GB 到 45MB,缩小了 96%。根据你的项目情况选合适的方案就好,不一定要到 Native Image 那一步。
## The Problem A basic Spring Boot Docker image using openjdk:17 weighed 1.2GB, significantly slowing deployments. ## Optimization Journey 1. **Base image swap** (1.2GB → 280MB): Switch to eclipse-temurin:17-jre-alpine 2. **Multi-stage build** (→ 195MB): Separate build and runtime stages 3. **jlink custom JRE** (→ 75MB): Build minimal JRE with only required modules 4. **Spring Boot layered jar**: 80% faster incremental builds via layer caching 5. **GraalVM Native Image** (→ 45MB): AOT compilation for ultimate size and 0.1s startup ## Result 96% size reduction from 1.2GB to 45MB. Choose the optimization level that fits your project's needs.