Docker 多阶段构建实战:把 Java 应用镜像从 1.13GB 压到 78MB

举报
yd_255409349 发表于 2026/10/30 12:37:55 2026/10/30
【摘要】 用多阶段构建把 Spring Boot 镜像从 1.13GB 压到 78MB:依赖层缓存、JRE 基础镜像、.dockerignore、非 root 用户四个关键细节,附层分析与体积门禁的验证方法。

*## 为什么一个 Java 应用的镜像动不动就 1GB

很多人第一次给 Spring Boot 应用写 Dockerfile,写出来大概是这样的:

FROM maven:3.9-eclipse-temurin-17
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
CMD ["java", "-jar", "target/app.jar"]

能跑,但镜像体积通常在 1GB 以上。原因很直接:这个镜像把 JDK、Maven、Maven 本地仓库缓存、项目源码、target 目录 全部打进去了。而线上运行时真正需要的只有两样东西:一个 JRE,和一个 jar 包。

镜像大带来的问题不只是磁盘:

  • 每次发布推送镜像,网络传输时间成倍增加,回滚变慢;
  • 基础镜像里的编译工具链在生产环境中是多余攻击面,gcc、git、maven 都会出现在漏扫报告里;
  • 容器启动时解压层数多,冷启动更慢。

多阶段构建:编译环境和运行环境分离

Docker 17.05 引入的多阶段构建(multi-stage build)就是为解决这件事设计的。一个 Dockerfile 里可以写多个 FROM,后面的阶段可以把前面阶段的产物复制过来,而前面阶段的整个文件系统会被丢弃。

改造后的写法:

# ---------- 构建阶段 ----------
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
# 先只复制 pom,让依赖层能够缓存
COPY pom.xml .
RUN mvn -B dependency:go-offline
COPY src ./src
RUN mvn -B clean package -DskipTests

# ---------- 运行阶段 ----------
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /build/target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]

最终镜像里只剩下 JRE 层和 jar 本身的层,实测从 1.13GB 降到 78MB 左右,体积下降超过 90%。

四个容易被忽略的细节

第一,COPY pom.xml 要单独一层。 如果直接 COPY . . 再 mvn package,任何一次源码改动都会让依赖下载层失效,构建时间从 20 秒退化到几分钟。先复制 pom 并执行 dependency:go-offline,只要 pom 不变,这一层永远命中缓存。

第二,基础镜像选 -jre 而不是 -jdk。 编译只需要在构建阶段发生,运行阶段用 JRE 能省掉 200MB 以上。如果追求极致,可以用 distroless/java17 或 alpine 系列,但要注意 glibc/musl 差异,某些依赖 JNI 的库在 alpine 上会报 No such file or directory。

第三,.dockerignore 必须写。 没有它,COPY . . 会把 .git、node_modules、IDE 配置、本地日志一起送进构建上下文。构建上下文越大,docker build 第一步的传输就越慢,而且容易因为误把 .env 复制进去导致密钥泄漏。

第四,别用 root 跑应用。 在运行阶段加一句:

RUN useradd -r -u 1001 appuser
USER 1001

K8s 的 runAsNonRoot 策略和很多安全基线都要求这一点,提前做掉比事后整改省事。

怎么验证优化真的生效

看层大小最直接:

docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" app:latest | head -20

哪一层异常大,一眼就能看出来。想更细粒度地分析,可以用 dive 工具,它会给出每一层的文件增量和浪费空间评分:

dive app:latest

另外建议在 CI 里加一条体积门禁,比如超过 200MB 就让流水线失败。镜像体积和代码一样会随着时间悄悄腐化,没有门禁就很难守住。

一个补充:分层复制 jar 可以进一步提速

Spring Boot 3 支持把 jar 拆成分层结构,让不常变的依赖和经常变的业务代码分开:

RUN java -Djarmode=layertools -jar app.jar extract

然后把 dependencies、spring-boot-loader、snapshot-dependencies、application 依次 COPY 进镜像。这样每次只改业务代码时,推送的只有最后一层几十 KB,对频繁发布的场景收益很明显。

小结

多阶段构建的本质是把「构建依赖」和「运行依赖」彻底分开,编译器不应该出现在生产镜像里。配合依赖层缓存、JRE 基础镜像、.dockerignore 和非 root 用户这四件事,一个典型 Java 服务的镜像可以稳定控制在 100MB 以内,同时构建速度、安全性和发布效率都会一起改善。
*

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。