Why You Should be Using Spring Boot Docker Layers
4 CommentsThe 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 /target
directory.
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.
Shahzada Hatim Mushtaq
thanks for the nice writeup. Just wanted to share two things
I was playing around with size of docker images and I added jlink to the mix (to create a small JRE). Resulting docker file here https://github.com/geoaxis/spring-boot-smallest-docker/blob/master/Dockerfile (and its ~72MB).
I also used dive to check out layer sizes https://github.com/wagoodman/dive
cheers
Krzysztof Szewczyk
Just use jib for creating ur images
Hashem
Thanks for the post, I followed the steps and getting the following error in the docker build:
“`
Step 4/23 : COPY ${JAR_FILE} application.jar
When using COPY with more than one source file, the destination must be a directory and end with a /
“`
it has several jar files in target/
mvn clean package works fine and passes all the steps
Srinivasa Govada
Thanks for article, this is very useful to me. However when I followed with the same steps, I don’t find any errors but I am facing with the problems
1. COPY command is not creating application.jar
2. extract command is not extracting any dependency folders are creating.
I am using docker version 18+ installed in my windows system. I tried using both Linux and Windows containers.