Why You Should be Using Spring Boot Docker Layers

Why You Should be Using Spring Boot Docker Layers

2 Comments

The Need for Spring Boot Docker Layers

If you follow my work, you know I’m a big fan of using Docker.

As a software engineer at Velo Payments, I use Docker on a daily basis. Our architecture is primarily Spring Boot Microservices, deployed in Docker containers.

This architecture gives us a tremendous amount of flexibility and scalability.

If you’ve lived with Docker for sometime, you’ll know that one thorn with Docker is the amount of disk space that and get consumed with Docker images.

Hypothetically speaking, let’s say you have a Docker host running 12 microservices in containers. Let’s say the image for each microservice container takes 200 MB of disk space.

Now, let’s also say you’re doing continuous deployments. Everytime a release is performed, another 200 MB image. The previous image does not go away, a new one is downloaded from the repository.

Multiply this by 12 microservices and overtime, a lot of disk space can be consumed.

This is going to be true if you’re just using Docker, Docker Swarm, or Kubernetes. It’s just the nature of how Docker images and layers work.

What if you could change your build process so that instead of 200 MB per release so that only 100 KB is consumed? A fraction of what was previously needed.

This is exactly where using Spring Boot Docker Layers can help.

Overview of Docker Images and Layers

Without getting overly technical, a Docker image is a collection of layers.

Each layer is an immutable TAR archive with a hash code generated from the file.

When you build a Docker image, each command which add files, will result in a layer being created.

The Spring Boot build process, builds an executable fat JAR. This is a jar contains your application class files and all the JARs for your dependencies.

It’s not uncommon to see these fat JARs growing to over 100MB.

The vast majority of the file data is from the dependencies.

Your application class files might only be a few hundred KB.

Spring Boot Docker Layers allows you to separate your dependencies and application class files into different layers.

This allows your dependency layers to be re-used when possible, significantly reducing the size of new releases.

Maven Configuration for Spring Boot Docker Layers

Support for Docker Layers is a new feature found in Spring Boot 2.3.0. You must be running Spring Boot 2.3.0.RELEASE or higher for these directions to work.

Note: directions for layer configuration in the release version are slightly different from the Spring Boot 2.3 Milestone releases.

To enable the packaging of layers in the Maven build process, add the following configuration to your Maven POM.

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <layers>
          <enabled>true</enabled>
          <includeLayerTools>true</includeLayerTools>
        </layers>
      </configuration>
    </plugin>
  </plugins>
</build>

Spring Boot will continue to produce a single fat JAR, but the packaging of the JAR is now ‘layered’.

We will use the Spring Boot layer tools to extract the layer files into our Docker image.

Spring Boot Layer Tools

The above Maven configuration tells Spring Boot to add layer tools into the fat JAR.

To generate the fat JAR, use the command:

mvn package

You will find the fat JAR in the root of the /targetdirectory.

To list the layers packaged inside the JAR archive, use this command:

java -Djarmode=layertools -jar my-app.jar list

To extract the layers, use this command:

java -Djarmode=layertools -jar my-app.jar extract

Layers will be extracted to the following folders:

/dependencies
/spring-boot-loader
/snapshot-dependencies
/application

All of the fat JAR dependencies are in  /dependencies. And your application class files are in /application.

If you would like to customize how the layers are extracted, please refer to the Spring Boot Maven plugin documentation here.

Multi-Stage Docker Build

We will use a multi-stage Docker build to first extract the files and then build our desired Docker image.

Stage 1 – Builder

Here are the stage one Dockerfile commands:

FROM openjdk:11-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

These Docker file commands do the following:

  • Start will the OpenJDK Java 11 JRE Slim image
  • Create working directory called /application
  • Copies the Spring Boot fat JAR into the working directory
  • Calls the Spring Boot layer tools to extract the layer files

Stage 2 – Spring Boot Application Image

Dockerfile commands:

FROM openjdk:11-jre-slim
WORKDIR application
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"]

These Dockerfile commands do the following:

  • Starts with the OpenJDK Java 11 JRE Slim image
  • Creates working directory called /application
  • Copies each layer directory into image
  • Sets the entry point for the image

Note: Remember, in the above, each COPY command will create an image layer. Thus, if your dependencies are not changing, a new layer is not created.

Complete Dockerfile

Here is the complete Dockerfile

FROM openjdk:11-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:11-jre-slim
WORKDIR application
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"]

Example of Spring Boot Microservices with Docker Layers

To show you the results, I’ve setup two faux Spring Boot Microservices. I say faux, they compile, build, and start okay. I added some controller code, but its untested – not meant to demonstrate Spring functionality.

You can find the complete source code in my GitHub repository here in the modules docker-layer-svc1 and docker-layer-svc2.

These are two different microservices, with different application code, but share the same Spring dependencies.

After several builds, I made a source code change and re-packaged docker-layer-svc1 using:

mvn package

To rebuild the Docker image, I’ll use this command:

docker build . --tag svc1

This command produces the following output:

Sending build context to Docker daemon  41.87MB
Step 1/12 : FROM openjdk:11-jre-slim 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} application.jar
 ---> c107bce376f9
Step 5/12 : RUN java -Djarmode=layertools -jar application.jar extract
 ---> Running in 7a6dfd889b0e
Removing intermediate container 7a6dfd889b0e
 ---> edb00225ad75
Step 6/12 : FROM openjdk:11-jre-slim
 ---> 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 svc1:latest

Notice how every how all the copy steps except Step 11 says ‘using cache’? Docker is using cached layers since they did not change.

Using the command:

docker history svc1

Produces the following output (base image history omitted):

IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
8575cc3ac2e3        About a minute ago   /bin/sh -c #(nop)  ENTRYPOINT ["java" "org.s…   0B                  
3a5f60a9b204        About a minute ago   /bin/sh -c #(nop) COPY dir:0cea19e682012ea7b…   54.1kB              
f928837acc47        4 hours ago          /bin/sh -c #(nop) COPY dir:e20e0f7d3984c5fba…   0B                  
e3861c690a96        4 hours ago          /bin/sh -c #(nop) COPY dir:9ef30157c6318a2d8…   224kB               
c9a01ed348a9        4 hours ago          /bin/sh -c #(nop) COPY dir:124320f4334c6319e…   41.5MB              
b6b89995bd66        5 hours ago          /bin/sh -c #(nop) WORKDIR /application          0B

You can see even in this modest Spring Boot faux microservice, the dependencies are 41.5MB, and the application classes are just 54.1 kb.

Changing to the docker-layer-svc2 module, I made a small source code change and repackaged it, then rebuilt the Docker image as above.

The Docker history output for service 2 is:

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
b328f4d5f61a        6 seconds ago       /bin/sh -c #(nop)  ENTRYPOINT ["java" "org.s…   0B                  
aca4b7a5f92a        7 seconds ago       /bin/sh -c #(nop) COPY dir:7a586cf8680e2bd04…   55.7kB              
f928837acc47        4 hours ago         /bin/sh -c #(nop) COPY dir:e20e0f7d3984c5fba…   0B                  
e3861c690a96        4 hours ago         /bin/sh -c #(nop) COPY dir:9ef30157c6318a2d8…   224kB               
c9a01ed348a9        4 hours ago         /bin/sh -c #(nop) COPY dir:124320f4334c6319e…   41.5MB              
b6b89995bd66        5 hours ago         /bin/sh -c #(nop) WORKDIR /application          0B

With the Service 2 history, you can see how the two services share the layer for dependencies, and have different layers for the application class files.

Conclusion

From this demonstration you can see how much space can be saved with each deployment. By using Spring Boot Docker Layers, you are isolating what is changing in your Docker image builds.

It’s a fairly common practice to use a common base image. This also limits the number of layers on the Docker host system.

You’re achieving something similar by having a common dependency layer. As you can see above, where the dependencies are the same, Docker will use that layer for multiple images.

About jt

    You May Also Like