Spring Boot 应用使用 spring-boot-maven-plugin 打包的是 uber jar(也就是常说的 fat jar)。
开发时大部分情况下只有应用程序对应的 jar 包有修改,但使用 uber jar 时对于 Docker 来说,复制 jar 的这一层是有变动的,就需要对整个一层做推送或拉取。
如果能将应用所在的 jar 包单独放在一层,不就可以减少镜像变动的大小了吗。
下面的 Dockerfile 提供了一个通用的示例 ^1^:
dockerfile
# Perform the extraction in a separate builder container
FROM bellsoft/liberica-openjre-debian:17-cds AS builder
WORKDIR /builder
# This points to the built jar file in the target folder
# Adjust this to 'build/libs/*.jar' if you're using Gradle
ARG JAR_FILE=target/*.jar
# Copy the jar file to the working directory and rename it to application.jar
COPY ${JAR_FILE} application.jar
# Extract the jar file using an efficient layout
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted
# This is the runtime container
FROM bellsoft/liberica-openjre-debian:17-cds
WORKDIR /application
# Copy the extracted jar contents from the builder container into the working directory in the runtime container
# Every copy step creates a new docker layer
# This allows docker to only pull the changes it really needs
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/ ./
# Execute the CDS training run
RUN java -XX:ArchiveClassesAtExit=application.jsa -Dspring.context.exit=onRefresh -jar application.jar
# Start the application jar with CDS enabled - this is not the uber jar used by the builder
# This jar only contains application code and references to the extracted jar files
# This layout is efficient to start up and CDS friendly
ENTRYPOINT ["java", "-XX:SharedArchiveFile=application.jsa", "-jar", "application.jar"]
这个 Dockerfile 主要分为两部分:
- 上面的部分是构建的 uber jar 按照分层分别提取到对应的目录;
- 下面的部分是将提取的目录复制到镜像。
- 由于是分别复制的,对应的层如果没有变化,Docker 就不会重新拉取。
- 这部分还预生成了 CDS 缓存文件,用于加快应用启动的速度。
开发环境:
- Spring Boot 3.4.3
- JDK 17
下面以之前的一个 WebSocket 示例项目为基础,在本地执行一下提取的命令看一下效果:
bash
java -Djarmode=tools -jar application.jar extract --layers --destination extracted
提取的目录: ^2^
dependencies
(for regular released dependencies)
spring-boot-loader
(for everything underorg/springframework/boot/loader
)
snapshot-dependencies
(for snapshot dependencies)
application
(for application classes and resources)
每个目录对应一个 Docker 层。
其实也可以解压缩 jar 文件,然后将文件复制到对应的目录。
解压缩后可以在 BOOT-INF 目录下看到两个 .idx 后缀的文件。这两个就是用来配置分层的。如果需要修改分层的配置,可以参考官方文档:Custom Layers Configuration。
layers.idx
md
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
classpath.idx
md
- "BOOT-INF/lib/spring-boot-3.4.3.jar"
- "BOOT-INF/lib/spring-boot-autoconfigure-3.4.3.jar"
- "BOOT-INF/lib/logback-classic-1.5.16.jar"
- "BOOT-INF/lib/logback-core-1.5.16.jar"
- "BOOT-INF/lib/log4j-to-slf4j-2.24.3.jar"
- "BOOT-INF/lib/log4j-api-2.24.3.jar"
- "BOOT-INF/lib/jul-to-slf4j-2.0.16.jar"
- "BOOT-INF/lib/jakarta.annotation-api-2.1.1.jar"
- "BOOT-INF/lib/snakeyaml-2.3.jar"
- "BOOT-INF/lib/jackson-databind-2.18.2.jar"
- "BOOT-INF/lib/jackson-annotations-2.18.2.jar"
- "BOOT-INF/lib/jackson-core-2.18.2.jar"
- "BOOT-INF/lib/jackson-datatype-jdk8-2.18.2.jar"
- "BOOT-INF/lib/jackson-datatype-jsr310-2.18.2.jar"
- "BOOT-INF/lib/jackson-module-parameter-names-2.18.2.jar"
- "BOOT-INF/lib/tomcat-embed-core-10.1.36.jar"
- "BOOT-INF/lib/tomcat-embed-el-10.1.36.jar"
- "BOOT-INF/lib/tomcat-embed-websocket-10.1.36.jar"
- "BOOT-INF/lib/spring-web-6.2.3.jar"
- "BOOT-INF/lib/spring-beans-6.2.3.jar"
- "BOOT-INF/lib/micrometer-observation-1.14.4.jar"
- "BOOT-INF/lib/micrometer-commons-1.14.4.jar"
- "BOOT-INF/lib/spring-webmvc-6.2.3.jar"
- "BOOT-INF/lib/spring-aop-6.2.3.jar"
- "BOOT-INF/lib/spring-context-6.2.3.jar"
- "BOOT-INF/lib/spring-expression-6.2.3.jar"
- "BOOT-INF/lib/spring-messaging-6.2.3.jar"
- "BOOT-INF/lib/spring-websocket-6.2.3.jar"
- "BOOT-INF/lib/slf4j-api-2.0.16.jar"
- "BOOT-INF/lib/spring-core-6.2.3.jar"
- "BOOT-INF/lib/spring-jcl-6.2.3.jar"
- "BOOT-INF/lib/spring-boot-jarmode-tools-3.4.3.jar"
拉取基础镜像(可选):
如果拉取 bellsoft/liberica-openjre-debian:17-cds 基础镜像比较困难,可以使用这个阿里云的镜像地址,之后再修改下镜像 tag 即可。
bash
docker pull registry.cn-hangzhou.aliyuncs.com/pusher/liberica-openjre-debian:17-cds
docker tag registry.cn-hangzhou.aliyuncs.com/pusher/liberica-openjre-debian:17-cds bellsoft/liberica-openjre-debian:17-cds
项目打包:
bash
mvn clean package -DskipTests
构建镜像:
bash
docker build -t liujiajia/websocket:1.0 .
运行镜像:
bash
docker run -p 8080:8080 liujiajia/websocket:1.0
查看容器:
bash
docker ps
进入容器:
bash
docker exec -it {container-id} bash
查看 application 目录下的文件:
bash
root@c9d989a4a0e3:/application# ls -l
total 32304
-rw-r--r-- 1 root root 9552 Mar 25 05:46 application.jar
-r--r--r-- 1 root root 33062912 Mar 25 05:46 application.jsa
drwxr-xr-x 2 root root 4096 Mar 25 05:46 lib
application.jsa 就是预生成的 CDS 缓存文件。
这个文件比较大,会导致镜像的总大小变大,但是可以带来应用启动速度的提升。以本地开发的 WebSocket 示例项目为例,正常启动大约 3.7s,而使用 CDS 缓存文件启动大约 2.4s。
如果不需要的话,可以从 Dockerfile 中移除生成这个文件的步骤以及最后的 -XX:SharedArchiveFile=application.jsa
参数。