用多阶段构建可将openjdk应用镜像从500mb+压缩至约120mb,关键在于builder阶段仅用于编译且不进入最终镜像,runner阶段必须使用纯jre镜像(如eclipse-temurin:17-jre-alpine)并精确copy jar文件。

直接结论:用多阶段构建能把 OpenJDK 应用镜像从 500MB+ 压到 120MB 左右,关键不是“用了多阶段”,而是 builder 阶段不带 JDK 运行时、runner 阶段不用完整 JDK 镜像。
builder 阶段必须用 maven/gradle 完整镜像,但不能进最终镜像
OpenJDK 应用(如 Spring Boot)需要编译、打包,builder 阶段得有 JDK 和构建工具。常见错误是直接用 maven:3.9-openjdk-17 打包完就 COPY target/*.jar —— 这没问题,但很多人忘了:这个镜像本身含完整 JDK,体积超 400MB,如果误把它当 runner 阶段用,就白干了。
- builder 阶段必须显式命名:
FROM maven:3.9-openjdk-17 AS builder,否则COPY --from=builder会报错 - 优先用
mvn package -DskipTests而非mvn clean package,避免重复清理导致缓存失效 - 如果项目用 Gradle,改用
gradle:8.5-jdk17,同样加AS builder;别用openjdk:17-jdk-slim自己装 Gradle,徒增层数和体积
runner 阶段禁用 openjdk:xx-jdk 和 openjdk:xx-slim
这两个标签名里带 jdk 或没明确写 jre 的镜像,都含 javac、javadoc、jdb 等运行时完全不需要的二进制,光这部分就占 100MB+。真正该用的是只含 JRE 的精简镜像。
- 首选
eclipse-temurin:17-jre-alpine(约 80MB)或amazoncorretto:17-jre-alpine(约 95MB),它们不含 shell,攻击面小 - 慎用
scratch:Java 应用依赖glibc和ca-certificates,直接跑会报No such file or directory;若硬要用,得自己COPY这些文件,维护成本高 - 别信 “alpine 就一定小”:某些 alpine 镜像因 musl libc 兼容问题被迫回退到
debian-slim,体积反而更大;验证方式很简单:docker run --rm -it eclipse-temurin:17-jre-alpine java -version
COPY 产物必须精确到文件,且 runner 阶段不装任何额外工具
多阶段构建后体积反弹,90% 出在这一层:COPY 路径太宽、RUN 指令又悄悄装东西。
- 只 COPY jar 文件:
COPY --from=builder /app/target/app.jar .,别写/app/target/整个目录,里面可能有original-app.jar、classes/等冗余内容 - runner 阶段禁止出现
RUN apt-get install或RUN apk add—— 除非真需要ca-certificates(alpine 镜像需显式装)或tzdata(时区需求),其他一律免谈 - Spring Boot 2.7+ 用户注意:
javax.*包已移除,若用老版openjdk:17-jre可能报NoClassDefFoundError,换eclipse-temurin:17-jre-focal(基于 Ubuntu)更稳
验证是否真的瘦身成功,别只看 docker images
docker images 显示的 SIZE 是压缩后大小,看不出镜像里塞了什么。真正要查的是分层结构和内容构成。
- 运行
docker history your-image-name:最终镜像应只有 2–3 层(基础镜像层 + COPY jar 层 + CMD 层),每层体积不超过几 MB;若看到几百 MB 的 RUN 层,说明 builder 阶段残留没清干净 - 用
dive your-image-name(需提前安装)逐层查看文件分布,重点检查/usr/lib/jvm/下有没有bin/javac、lib/tools.jar等 JDK 工具链文件 - 启动容器后执行
ls -la /usr/lib/jvm/:JRE 镜像里应该只有jre/子目录,没有jdk/或顶层bin/javac
最常被忽略的一点:多阶段构建本身不保证瘦身,它只是提供了一个隔离机制。真正决定体积的,是你在 builder 阶段有没有删掉 ~/.m2/repository 缓存、在 runner 阶段有没有误用带 shell 的镜像、以及 COPY 路径是否足够克制。这些细节不抠,镜像照样卡在 400MB 上下。











