More and more projects are containerized and Docker has become an important tool in software development. We can usually package the fat jar of a Spring Boot application as a docker image with the following Dockerfile.

1
2
3
4
5
FROM adoptopenjdk:8-jre-hotspot
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

It looks good, but you will find that if we change the business code, the image will be rebuilt, even if you just change a string. If you use CI/CD to build and deploy Docker images, you will find that sometimes the build time in the CI pipeline can be very long, or even stuck, especially when there are more images. So we project to optimize this place.

Docker’s layering mechanism

To optimize it, we need to understand the build hierarchy of Docker images. Because Docker image consists of many layers, each layer represents a command in the Dockerfile. Each layer is an increment of the changes on the base layer, and incremental builds are done from the bottom up.

Docker

This mechanism is actually where Docker gets its name from, like dock workers yarding cargo.

docker

Also when we build a Docker image, it is extracted in layers and cached in the host, and these layers can be reused, which gives us the opportunity to optimize.

Like the container above, if we put the container that is prone to change on the bottom layer, every time we change it we need to remove the top of it; if we put it on the bottom layer we can reduce the workload. The same is true for the build of Docker images.

Optimization of Spring Boot images

Spring Boot’s fat jar can be significantly more efficient if it can be split into layers to build one layer and reuse the duplicate layers from the host cache. So Spring Boot applications can be divided into layers according to the frequency of changes as follows.

  • dependencies (dependencies generally don’t change much)
  • spring-boot-loader (the spring boot loader doesn’t change much either)
  • snapshot-dependencies (snapshot dependencies, for snapshot versions of dependencies, the update iterations will be faster)
  • application (business layer, which is what we change most often)

Since Spring Boot 3.x, Spring Boot provides the spring-boot-jarmode-layertools jar package, which will be added to the application jar as a dependency. To start the jar via the layertools jar mode.

1
$ java -Djarmode=layertools -jar my-app.jar

The index file layers.idx will be generated for the above four levels.

layers.idx

Above is the information about the Spring Boot application jar built in this schema, we can see these two things.

This feature relies on the spring-boot-maven-plugin plugin.

We just need to change the Dockerfile to the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 第一阶段使用 layertools 的 extract 命令将应用程序拆分为多个层  本次构建标记为builder
FROM adoptopenjdk:8-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract

#  第二阶段从分层中复制并构建镜像
FROM adoptopenjdk:8-jre-hotspot
WORKDIR application
# 从上面构建的builder 中复制层  注意保证层的顺序
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

You can then reuse the Docker caching layer in the host to speed up the build efficiency. The build command is.

1
docker build --build-arg JAR_FILE=path/to/myapp.jar . -tag demo

It will then output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Sending build context to Docker daemon  41.87MB
Step 1/12 : adoptopenjdk:8-jre-hotspot as builder
 – -> 973c18dbf567
Step 2/12 : WORKDIR application
 – -> Using cache
 – -> b6b89995bd66
Step 3/12 : ARG JAR_FILE=target/*.jar
 – -> Using cache
 – -> 2065a4ad00d4
Step 4/12 : COPY ${JAR_FILE} app.jar
 – -> c107bce376f9
Step 5/12 : RUN java -Djarmode=layertools -jar app.jar extract
 – -> Running in 7a6dfd889b0e
Removing intermediate container 7a6dfd889b0e
 – -> edb00225ad75
Step 6/12 : FROM  adoptopenjdk:8-jre-hotspot
 – -> 973c18dbf567
Step 7/12 : WORKDIR application
 – -> Using cache
 – -> b6b89995bd66
Step 8/12 : COPY – from=builder application/dependencies/ ./
 – -> Using cache
 – -> c9a01ed348a9
Step 9/12 : COPY – from=builder application/spring-boot-loader/ ./
 – -> Using cache
 – -> e3861c690a96
Step 10/12 : COPY – from=builder application/snapshot-dependencies/ ./
 – -> Using cache
 – -> f928837acc47
Step 11/12 : COPY – from=builder application/application/ ./
 – -> 3a5f60a9b204
Step 12/12 : ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
 – -> Running in f1eb4befc4e0
Removing intermediate container f1eb4befc4e0
 – -> 8575cc3ac2e3
Successfully built 8575cc3ac2e3
Successfully tagged demo:latest

java -Djarmode=layertools -jar is not recommended for use in startup scripts, only recommended for use in builds .

Additions

Regarding the plugin there are currently version differences, in Spring Boot 2.3 you need to add the following configuration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
          <layers>
            <enabled>true</enabled>
          </layers>
      </configuration>
    </plugin>
    ...
  </plugins>
  ...
</build>

By default in springboot 2.4.x and above.

1
2
3
4
5
6
7
8
9
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <!--当然你可以修改layers.enabled为false以关闭此功能。-->
        </plugin>
    </plugins>
</build>

By enabling the layered build Spring Boot application image, you can obviously feel the speed of pushing the image a lot faster (of course, this is the case of the dependency has not changed); in addition, when pulling the image from the remote only need to pull the change layer, the speed is also significantly faster; on the build is actually built twice, although with the cache, the efficiency actually does not change much. That is to say, in the mirror of the network transmission on the layered build has obvious advantages, worth a try.

Reference https://felord.cn/docker-layer-spring-boot.html