Using layered docker images over fat-jar docker images in spring boot application
Tags: docker, springboot, java, programming
Published: 2023-02-01 16:24:52

---
title: Using layered docker images over fat-jar docker images in spring boot application
published: true
date: 2023-02-01 16:24:22 UTC
tags: docker,springboot,java,programming
canonical_url: https://blog.dripto.xyz/using-layered-docker-images-over-fat-jar-docker-images-in-spring-boot-application
cover_image: https://cdn.hashnode.com/res/hashnode/image/upload/v1674062100538/31881857-1a6d-4a9d-8215-25372eb7fa2c.jpeg?w=1600&h=840&fit=crop&crop=entropy&auto=compress,format&format=webp
---

## 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 `Dockerfile`s 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:

```bash
 - "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:

```bash
 java -Djarmode=layertools -jar target/<jar_name>.jar list
```
This will provide a simplistic view of the content of the `layers.idx` file.

```bash
 dependencies
 spring-boot-loader
 snapshot-dependencies
 application
```
We can also extract the layers into directories:
```bash
 java -Djarmode=layertools -jar target/<jar_name>.jar extract
```

the `application` 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.

* First, let's add the fat jar file to the base image:
```bash
 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
```

* Next, we extract the layers of the artifact using the following command:
```bash
 RUN java -Djarmode=layertools -jar app.jar extract
```

* Then, we copy the layers from the builder image to the actual image:
```bash
 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/ ./
```

* Finally, we start the image using `org.springframework.boot.loader.JarLauncher` and expose the necessary ports:
```bash
 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](https://www.baeldung.com/docker-layers-spring-boot)

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

3. [https://www.youtube.com/watch?v=hAHXp\_jQWVo](https://www.youtube.com/watch?v=hAHXp_jQWVo)