跳到主要内容

第一次把 Spring Boot 项目打成镜像,我看到 1.2GB 这个数字时以为是命令敲错了。推到内网仓库要三分钟,节点拉取又要三分钟,一个简单的滚动发布能拖到十分钟。

排查下来,镜像里塞了 JDK、Maven、源码、测试代码,以及 .m2 里几百兆的依赖缓存——全都不是运行时需要的东西。

问题从哪来 ​

先看一个"朴素"的 Dockerfile,问题很典型:

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

它做了什么?把构建环境原封不动地留到了运行镜像里:

内容运行时需要吗体积
完整 JDK(含编译器)不需要,JRE 就够~300MB
Maven + ~/.m2 依赖不需要~400MB
源码、测试代码不需要~10MB
目标 jar✅ 需要~80MB

结论:真正需要的只有最后一行。

多阶段构建 ​

核心思路:用多个 FROM,前面的阶段只负责产出产物,最后一个阶段只装运行时。

dockerfile
# ---------- 阶段 1:构建 ----------
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /build

# 先只复制 pom,利用 Docker 层缓存
COPY pom.xml .
RUN mvn -B dependency:go-offline

COPY src ./src
RUN mvn -B clean package -DskipTests

# ---------- 阶段 2:运行 ----------
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

# 从构建阶段只取产物
COPY --from=builder /build/target/app.jar app.jar

# 非 root 运行
RUN addgroup -S app && adduser -S app -G app
USER app

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

三个关键点:

  1. AS builder + COPY --from=builder:跨阶段只搬运需要的文件,其余全部丢弃
  2. dependency:go-offline 单独成层:依赖没变时,这一层直接命中缓存,构建从 3 分钟降到 20 秒
  3. 运行阶段用 -jre-alpine:只要运行时,不要编译器

顺序很重要

COPY pom.xml 必须在 COPY src 之前。改一行代码不会让依赖层失效,这是缓存命中的前提。

瘦身清单 ​

做完多阶段构建通常是 200MB 左右。再往下压,按收益排序:

1. 别用 fat jar 启动(收益最大) ​

Spring Boot fat jar 启动时解压全部依赖,慢且占用高。拆成分层目录后,可以只复制变动的层:

dockerfile
FROM eclipse-temurin:21-jre-alpine AS extractor
WORKDIR /build
COPY --from=builder /build/target/app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=extractor /build/dependencies/ ./
COPY --from=extractor /build/spring-boot-loader/ ./
COPY --from=extractor /build/snapshot-dependencies/ ./
COPY --from=extractor /build/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

业务代码改动时,只有最后一层(通常几 MB)需要重新推送和拉取。

2. 加 .dockerignore ​

text
target/
.git/
.idea/
*.log
node_modules/

少了它,COPY . . 会把 target/ 里的旧产物也塞进构建上下文,既慢又容易出错。

3. 清掉包管理器缓存 ​

dockerfile
RUN apk add --no-cache tzdata curl \
    && rm -rf /var/cache/apk/*

注意 --no-cache 和 rm -rf 必须和 apk add 在同一个 RUN 里,否则删除层不会减少镜像体积。

4. 用 distroless(可选,追求极致安全) ​

dockerfile
FROM gcr.io/distroless/java21-debian12
COPY --from=builder /build/target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

没有 shell、没有包管理器,攻击面最小。代价是没法 docker exec 进去调试,出问题只能靠日志和监控。

别用 latest 标签

FROM maven:latest 会在某天悄悄换掉 JDK 版本。生产镜像必须锁死基础镜像版本,例如 maven:3.9-eclipse-temurin-21。

验证与对比 ​

bash
# 看体积
docker images myapp --format "{{.Repository}}:{{.Tag}} {{.Size}}"

# 看每一层占了多少,定位大块头
docker history myapp:1.0 --human --format "{{.Size}}\t{{.CreatedBy}}"

# 看镜像里到底有什么文件
dive myapp:1.0

我这轮优化的实际数据:

阶段体积构建耗时推送耗时
优化前1.2 GB3m10s2m50s
多阶段 + JRE210 MB1m05s35s
分层 + .dockerignore122 MB22s(命中缓存)8s

最后一个数字才是关键:日常迭代时镜像推送从近 3 分钟降到 8 秒,发布体验完全不同。

小结 ​

  • 多阶段构建解决"构建环境混进运行镜像",是收益最大的一步
  • pom 先复制,锁死基础镜像版本,配好 .dockerignore
  • 分层启动 + 清理缓存同层执行,能再压掉一半
  • 先用 docker history 定位谁在占空间,再动手,别凭感觉优化

最后更新于:

本站内容采用 CC BY-NC 4.0 许可