Using layered docker images over fat-jar docker images in spring boot application

Using layered docker images over fat-jar docker images in spring boot application

ยท

6 min read

TL;DR

To achieve more efficient docker image building and faster startup times, instead of doing this,

FROM eclipse-temurin:17-jdk
ARG ARG_VERSION
ARG APP_NAME=test-app

EXPOSE 8080

WORKDIR app
ARG JAR_FILE=build/libs/${APP_NAME}-${ARG_VERSION}.jar

ADD ${JAR_FILE} app.jar

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

Do this,

FROM eclipse-temurin:17-jdk as builder
ARG ARG_VERSION
ARG APP_NAME=test-app

ARG JAR_FILE=build/libs/${APP_NAME}-${ARG_VERSION}.jar
WORKDIR app

COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:17-jdk
WORKDIR /app

COPY --from=builder app/dependencies/ ./
COPY --from=builder app/spring-boot-loader/ ./
COPY --from=builder app/snapshot-dependencies/ ./
COPY --from=builder app/application/ ./

EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

The problem

Even though considered archaic by some, using Dockerfile is still one of the more flexible ways to create a docker image (and contrary to popular belief, it doesn't always require a docker daemon to be present, i.e. check out kaniko). In our organization, it is very common to build docker images for Spring-boot applications via a Dockerfile for deployment purposes.

One of the main issues is that over time, the use of a traditional Dockerfiles can consume disk space exponentially.

To illustrate this, let's take an example of a typical build of a Spring Boot repository. This build creates what is known as a fat-jar, which is a JAR file that contains not only the Java program but also embeds its dependencies. Since it contains a lot of dependencies, it is not uncommon for a fat-jar of a fairly complex project to be around 100MB in size.

Now, consider an organization that has around 50 microservices. With each build of these microservices, the organization will be generating around 5GB of data (100MB x 50). If these services are built around 60 times a month (twice a day on average, though it should be much more if done in a CI pipeline) and each build creates and deploys its own docker image, this will take up around 300GB of storage at least per month. As you can see, this can quickly become an issue as the number of services and builds increases.

What can be done?

As we've seen, the use of fat jar images can lead to a significant increase in disk space usage over time. However, it's worth noting that even though the images are built multiple times, only a small fraction of it actually changes per build. The dependencies largely stay the same, and only the application code changes. In fact, the application code that changes is usually quite small in comparison to the entire image, often being less than 10MB. By keeping this information in mind, we can leverage Docker image layers to our advantage. By using layers, we can create more efficient and secure images that are faster to build and deploy.

Understanding Docker Images and Layers

A Docker image is a collection of layers. Each layer is an immutable TAR archive with a hash code generated from the file. When building a Docker image, each command that adds files will result in a layer being created. These layers can be cached and reused in future builds, making the process more efficient.

When using a fat-jar image, the fat-jar is added directly to the Dockerfile. This creates a Docker image with a single layer of application and dependencies. As a result, every single change to the fat jar (even for a single file change) will create a new layer and thus adding to the issue of disk space consumption as described previously. By leveraging layers, we can optimize the space usage as well as making the process of building and deploying the images faster.

Spring Boot and Layered Jars

Spring Boot version 2.3.0 and above offers two new features to improve the generation of Docker images:

  1. Buildpack support: This feature provides the Java runtime for the application, allowing for the automatic building of the Docker image without the need for a Dockerfile. However, this feature is out of scope for this blog post.

  2. Layered jars: This feature helps to optimize the Docker layer generation process. If the Spring Boot jar is created using the spring-boot-maven-plugin or spring-boot-gradle-plugin, the jar file comes pre-created with 4 layers. The BOOT-INF/layers.idx file records the different layers and can be checked by extracting the jar. A sample BOOT-INF/layers.idx file might look like this:

     - "dependencies":
       - "BOOT-INF/lib/"
     - "spring-boot-loader":
       - "org/"
     - "snapshot-dependencies":
     - "application":
       - "BOOT-INF/classes/"
       - "BOOT-INF/classpath.idx"
       - "BOOT-INF/layers.idx"
       - "META-INF/"
    

    The layers can also be inspected by using the command:

     java -Djarmode=layertools -jar target/<jar_name>.jar list
    

    This will provide a simplistic view of the content of the layers.idx file.

     dependencies
     spring-boot-loader
     snapshot-dependencies
     application
    

    We can also extract the layers into directories:

     java -Djarmode=layertools -jar target/<jar_name>.jar extract
    

    changes the most often. It hosts the actual code for the application. By storing each layer as an individual Docker image layer, we can optimize the space usage and speed up the build and deployment process. Only the application layer will be changed with each build, while the other layers can be cached and reused from previous builds.

    So, how do we do this?

    We use multistage Docker builds.

Multistage Dockerfiles

We use multistage dockerfiles to create layered docker images for our Spring Boot applications.

  1. First, let's add the fat jar file to the base image:

     FROM eclipse-temurin:17-jdk as builder
     ARG ARG_VERSION
     ARG APP_NAME=test-app
    
     ARG JAR_FILE=build/libs/${APP_NAME}-${ARG_VERSION}.jar
     WORKDIR app
    
     COPY ${JAR_FILE} app.jar
    
  2. Next, we extract the layers of the artifact using the following command:

     RUN java -Djarmode=layertools -jar app.jar extract
    
  3. Then, we copy the layers from the builder image to the actual image:

     FROM eclipse-temurin:17-jdk
     WORKDIR /app
    
     COPY --from=builder app/dependencies/ ./
     COPY --from=builder app/spring-boot-loader/ ./
     COPY --from=builder app/snapshot-dependencies/ ./
     COPY --from=builder app/application/ ./
    
  4. Finally, we start the image using org.springframework.boot.loader.JarLauncher and expose the necessary ports:

     EXPOSE 8080
     ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
    

Final Dockerfile:

FROM eclipse-temurin:17-jdk as builder
ARG ARG_VERSION
ARG APP_NAME=test-app

ARG JAR_FILE=build/libs/${APP_NAME}-${ARG_VERSION}.jar
WORKDIR app

COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:17-jdk
WORKDIR /app

COPY --from=builder app/dependencies/ ./
COPY --from=builder app/spring-boot-loader/ ./
COPY --from=builder app/snapshot-dependencies/ ./
COPY --from=builder app/application/ ./

EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

Additional Advantage:

Since the jar is already extracted in the docker image, the startup time is improved. On average, I can see a 1 - 1.5 sec improvement in the startup time across all my spring boot application.

Conclusion:

In conclusion, using layered docker images can greatly reduce the storage space consumed by your Spring Boot applications and improve the startup time of your services. By using multistage Dockerfiles, we can extract the layers of our fat jar and store them in individual image layers. This way, only the application layer will be changed per build and the other layers can be cached and reused from the previous builds. This not only saves storage space, but also improves the startup time of your services, providing an additional advantage to your organization.

Further Read:

  1. https://www.baeldung.com/docker-layers-spring-boot

  2. https://springframework.guru/why-you-should-be-using-spring-boot-docker-layers/

  3. https://www.youtube.com/watch?v=hAHXp_jQWVo

Did you find this article valuable?

Support Driptaroop Das by becoming a sponsor. Any amount is appreciated!

ย