背景
Spring Boot REST API 打成 jar 之后,放进容器常见有三层递进:先有一个能跑的 fat jar,再用分层 JAR 优化 Docker 缓存,最后用 Cloud Native Buildpacks 一键出镜像;若还要缩小运行时,可在 Buildpack 上显式开启 jlink 裁剪 JRE。fat jar 布局与 JarLauncher 见专文 Spring Boot Executable JAR。
Spring Framework 本身几乎不讲容器镜像;相关说明集中在 Spring Boot Reference 的 Packaging → Container Images。本文把分层 JAR、bootBuildImage、Paketo jlink 串成一条阅读路径,并附上官方文档索引。GraalVM Native Image 是另一条路线,见 GraalVM Native Image 简介。
三条路径总览
flowchart TB
subgraph build [构建]
SRC[源码 mvn package / bootJar]
FAT[Fat JAR 可执行 uber jar]
LAY[分层 JAR 含 layers.idx]
end
subgraph deploy [容器化]
D1[简单 Dockerfile
COPY jar + java -jar]
D2[分层 Dockerfile
jarmode extract --layers]
BP[bootBuildImage / build-image
Paketo Buildpacks]
end
subgraph runtime [运行时 JVM]
JRE[完整 JRE]
JLINK[jlink 精简 JRE
BP_JVM_JLINK_ENABLED]
end
SRC --> FAT
FAT --> LAY
FAT --> D1
LAY --> D2
LAY --> BP
D1 --> JRE
D2 --> JRE
BP --> JRE
BP -.->|可选| JLINK
| 路径 | 典型产物 | 主要收益 | 复杂度 |
|---|---|---|---|
| Fat jar + 简单镜像 | 单层 app.jar + JRE 基础镜像 |
上手最快 | 低 |
| 分层 JAR + Dockerfile | 多 COPY 层 + JRE |
推送镜像时依赖层可缓存 | 中 |
Buildpack(bootBuildImage) |
OCI 镜像,自动检测 jar | 零 Dockerfile、与 Paketo 生态一致 | 低~中 |
| Buildpack + jlink | 同上,运行时 JRE 更小 | 缩小镜像体积 | 中(需调 env) |
分层 JAR
分层 JAR 在 fat jar 布局不变的前提下,额外写入 BOOT-INF/layers.idx,声明哪些路径属于哪一层、层的写入顺序。Spring Boot 2.3 起默认开启;Maven repackage / Gradle bootJar 的 layered 无需额外配置即可得到。
若暂不做分层,最简容器化是把整个 fat jar 放进单层镜像:
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
能跑,但业务代码一改,依赖层也无法被 Docker 缓存复用。官方 Efficient Container Images 明确建议不要长期把 fat jar 原样塞进单层镜像;下文分层 JAR 与 Buildpack 即为此优化。
默认四层(从不易变到易变):
| 层名 | 典型内容 |
|---|---|
dependencies |
非 SNAPSHOT 的 BOOT-INF/lib/*.jar |
spring-boot-loader |
org/springframework/boot/loader/** |
snapshot-dependencies |
版本号含 SNAPSHOT 的依赖 |
application |
BOOT-INF/classes、清单、索引文件等 |
layers.idx 片段示例:
- "dependencies":
- BOOT-INF/lib/library1.jar
- "spring-boot-loader":
- org/springframework/boot/loader/launch/JarLauncher.class
- "snapshot-dependencies":
- "application":
- BOOT-INF/classes/
- META-INF/MANIFEST.MF
查看分层(需 jar 内带 spring-boot-jarmode-tools):
java -Djarmode=tools -jar my-app.jar list-layers
java -Djarmode=tools -jar my-app.jar extract --layers --destination extracted
手写 Dockerfile(jarmode)
官方 Dockerfiles 推荐多阶段构建:builder 阶段解压分层,runtime 阶段按层 COPY:
FROM eclipse-temurin:21-jre AS builder
WORKDIR /builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted
FROM eclipse-temurin:21-jre
WORKDIR /application
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
ENTRYPOINT ["java", "-jar", "application.jar"]
业务代码变更时,通常只需重建最后的 application 层;依赖层可命中 Docker 缓存。同一文档还介绍在该布局上叠加 AOT Cache / CDS 以缩短 JVM 冷启动(与分层正交,本文不展开)。
自定义分层规则见 Maven Packaging Layered Jar 或 Gradle Packaging Layered Jar or War。
Cloud Native Buildpacks(bootBuildImage)
CNB 规范与 Paketo 实现见专文 CNB: Cloud Native Buildpacks 与 Paketo。下文只保留与 Spring Boot 插件相关的要点。
Spring Boot 内置 Cloud Native Buildpacks 集成:插件把可执行 jar 交给 builder(默认 Paketo),buildpack 检测 artifact、安装 JRE、生成启动命令并产出 OCI 镜像。
./mvnw spring-boot:build-image
# Gradle
./gradlew bootBuildImage
要点:
- 无需先手动
repackage:插件会在需要时自动打可执行 jar。 - 需要本机可访问的 Docker daemon(或按插件文档配置远程构建)。
- 默认 builder 为
paketobuildpacks/builder-noble-java-tiny:latest(以当前 Spring Boot 插件文档为准)。 - Paketo Spring Boot buildpack 会读取 jar 中的
Spring-Boot-Layers-Index/layers.idx,日志里可见Creating slices from layers index,把应用切成多个镜像层——与上一节分层 JAR 设计对齐。
Maven 配置 JVM 版本示例(传给 Paketo 环境变量):
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<env>
<BP_JVM_VERSION>21</BP_JVM_VERSION>
</env>
</image>
</configuration>
</plugin>
Gradle 等价配置见 BootBuildImage API 文档中的 environment / builder。
Buildpack 路径下,运行时默认安装的是 JRE(BP_JVM_TYPE 默认为 JRE),不是 GraalVM Native;Native 需另开 BP_NATIVE_IMAGE=true 等配置,见 GraalVM Native Image 简介。
K8s CI:Tekton + buildpacks-phases
上一节的 bootBuildImage / pack build 属于 CNB 的 Platform 层:需要 Docker daemon 来拉 builder、起构建容器、把镜像写入本地。在 Kubernetes 里若已用 Tekton 与 Kaniko 做无 daemon 构建,不能把 Paketo 塞进 Kaniko,也不能在 Pod 里挂 docker.sock 跑 pack——后者需要 privileged,与 Kaniko 的安全模型相反。
CNB 真正干活的是 builder 镜像里的 lifecycle。无 Docker daemon 时,由 Platform 直接调 lifecycle 各 phase,并把镜像 push 到 registry。Tekton 侧官方做法是安装 Catalog 里的 buildpacks-phases Task:把 prepare、detect、build、export 等步骤拆成多个 Tekton step,而不是用 pack CLI。
PipelineRun
→ git-clone
→ buildpacks-phases(CNB lifecycle,无 daemon)
→ update-gitops / 部署
与现有 Tekton + Kaniko 完全并存:二者只是不同的 Task,共用同一套 Tekton 平台、workspace、registry Secret(如 nexus-docker-cred)。Go 等有 Containerfile 的项目继续 kaniko-build;Spring Boot 想零 Dockerfile 的 Pipeline 把 build step 换成 buildpacks-phases 即可。homelab 落地示例见 enx-api Homelab CI/CD。
典型 Pipeline 片段(在 clone 之后;若 workspace 里已是 fat jar,可省略 Maven 编译 step):
- name: build-image
taskRef:
name: buildpacks-phases
params:
- name: APP_IMAGE
value: docker-hosted.example.com/myapp:$(tasks.fetch-source.results.commit)
- name: CNB_BUILDER_IMAGE
value: paketobuildpacks/builder-noble-java-tiny:latest
- name: SOURCE_SUBPATH
value: repo/my-spring-app
- name: CNB_ENV_VARS
value:
- BP_JVM_VERSION=21
workspaces:
- name: source
workspace: source-code
要点:
- 输入:源码目录(Paketo Maven buildpack 会编译)或已
mvn package的 jar 所在目录;SOURCE_SUBPATH指向 workspace 内相对路径。 - 输出:
APP_IMAGE指定的远程镜像;无 daemon 时必须 push 到 registry,不能--publish=false只留本地。 - 环境变量:与
bootBuildImage相同,BP_JVM_VERSION、BP_JVM_JLINK_ENABLED等通过CNB_ENV_VARS传入。 - 产物行为:仍走 Paketo Spring Boot buildpack,会读
layers.idx切镜像层,与本地bootBuildImage一致。
三种 K8s 构建路径对比:
| 路径 | 需要 Docker daemon | 需要 Dockerfile | 典型场景 |
|---|---|---|---|
bootBuildImage / pack |
是 | 否 | 本机或 CI 有 Docker |
| Tekton + Kaniko | 否 | 是(含分层 JAR 模板) | 已有 Containerfile、多语言混部 |
| Tekton + buildpacks-phases | 否 | 否 | K8s 无 daemon、想走 Paketo |
Kaniko 与 buildpacks-phases 不是二选一的全局策略,而是 按项目选 Task。若 Pipeline 里还要跑测试、lint,失败则不构建,Tekton 串 step 比单独装 kpack Operator 更直接;kpack 适合平台级声明式镜像治理,与 Tekton 职责不同,本文不展开。
Buildpack + jlink 裁剪 JRE
jlink(JDK 9+ 模块系统)可从完整 JDK 生成只含所需模块的定制 JRE,仍用 HotSpot 跑字节码,与 GraalVM Native Image(AOT 成本地二进制)不同。手工多阶段 Dockerfile 里常用 jdeps + jlink;走 Paketo 时由 Java buildpack 代为执行。
重要事实:Paketo 文档写明 Install a Minimal JRE with JLink 中 BP_JVM_JLINK_ENABLED 默认为 false。即 bootBuildImage 默认装完整 JRE,不会自动 jlink;要缩小运行时需显式开启。
pack build my-app \
--path target/myapp.jar \
--env BP_JVM_JLINK_ENABLED=true
自定义 jlink 参数(传入后不再使用 Paketo 默认参数集):
pack build my-app \
--env BP_JVM_JLINK_ENABLED=true \
--env BP_JVM_JLINK_ARGS="--no-header-files --compress=1 --add-modules java.base,java.se"
在 Spring Boot 插件中通过 image.env 传入相同变量即可。典型场景:JVM 发行版只提供 JDK、镜像里被迫带完整 JDK 时,用 jlink 生成精简 JRE(Paketo 文档以 Amazon Corretto 为例)。
| 维度 | 默认 Buildpack(JRE) | Buildpack + jlink | GraalVM Native(另文) |
|---|---|---|---|
| 运行时 | HotSpot + 完整 JRE | HotSpot + 定制 JRE | Substrate VM,无 java -jar |
| 应用形态 | 字节码 fat jar | 字节码 fat jar | AOT 本地可执行文件 |
| 默认是否开启 | 是 | 否,需 BP_JVM_JLINK_ENABLED=true |
否,需 native profile / env |
| Spring AOT | 不需要 | 不需要 | 需要 |
jlink 模块裁剪与 JPMS 关系见 JPMS 与 Jigsaw;与 Spring AOT 无直接关系。
如何选型(简表)
| 场景 | 建议 |
|---|---|
| 本地试容器、学习 | Fat jar + 简单 Dockerfile |
| 自建 CI、要控制每一层 COPY | 分层 JAR + 官方 Dockerfile 模板 |
| 团队想少维护 Dockerfile、与 Paketo 生态一致 | bootBuildImage / spring-boot:build-image(本机有 Docker) |
| K8s CI 无 Docker daemon、仍想走 Paketo | Tekton + buildpacks-phases Task(与 Kaniko 可并存) |
| K8s CI 无 daemon、已有 Containerfile | Tekton + Kaniko + 分层 JAR Dockerfile |
| 镜像体积敏感、仍要完整 JVM 语义 | Buildpack + BP_JVM_JLINK_ENABLED=true,或手写 jlink 多阶段构建 |
| 冷启动毫秒级、接受 AOT 约束 | 见 GraalVM Native Image 简介,不是本文三条 JVM 路径的延伸 |
多数长期运行的 REST 服务:分层 JAR + Buildpack 或分层 Dockerfile 即可;jlink 在镜像体积成为瓶颈时再开。
官方文档索引
参考
- CNB: Cloud Native Buildpacks 与 Paketo
- Tekton 与 Kaniko
- enx-api Homelab CI/CD
- Spring Boot
- Spring Boot Executable JAR
- Spring AOT 简介
- GraalVM Native Image 简介
- JPMS 与 Jigsaw
维护记录
| 时间 | 修改内容 | 原因 |
|---|---|---|
| 2026-06-24 | 补充 K8s CI 小节(Tekton + buildpacks-phases);更新选型表与文档索引 | 说明无 Docker daemon 时如何在 Tekton 中与 Kaniko 并存使用 Paketo |
| 2026-06-24 | Fat JAR / JarLauncher 拆至 spring-boot-executable-jar.md;精简本文为容器打包主线 | 机制专文与容器化索引分工 |