In today’s fast-paced software development world, it’s essential to have tools that streamline your workflow and enable you to develop applications quickly and efficiently. Docker is one such tool that has gained immense popularity in recent years due to its ability to create a consistent environment for applications to run in. Java developers can particularly benefit from using Docker to build, package, and deploy their applications seamlessly. In this blog post, we will explore the benefits of using Docker for Java development and provide a step-by-step guide to help you get started with Dockerizing your Java applications.
What is Docker?
Docker is a toolkit that enables developers to easily pack, ship, and run any application as a lightweight, portable, self-sufficient container.
What are containers?
Containers are a standardized unit of software that allows developers to isolate their app from its environment, solving the “it works on my machine” headache.
What are docker images?
Docker image is a read-only template that contains a set of instructions for creating a container that can run on the Docker platform.
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 that adds files will result in a layer being created, and this is very important information that will be used later. So far so good, let’s go to the more interesting part.
Docker and Spring Boot
For our example, we will use Spring Boot to develop a simple Java REST API. There are multiple ways how we can Dockerize our Spring Boot Application. Let’s see how to use Docker layers so we can speed and improve our development and CI/CD processes.
Docker Image Layers and Spring Boot
The Spring Boot build process builds an executable fat JAR. This is a jar containing your application class files and all the JARs for your dependencies. From our experience, real-world Spring Boot applications can range in size from 50 MB – 250 MB, if not larger.
Single Layer Spring Boot Approach
In this example, we will see how we can build on the image that will contain our Spring Boot fat jar using a single image layer. You can find the sample application code used in this example on GitHub under the following link https://github.com/popovski/spring-boot-docker-single-layer.
Let’s create a new image that will contain and run our jar file
- Clone the project Git Clone “git@github.com:popovski/spring-boot-docker-single-layer.git”
- Build the jar file, and execute the command in the root directory of the project – “./mvnw clean package”
- The fat jar file can be found under the target folder
- Create the Docker image based on the docker file (see for sample below), using the command “docker build -t popovskinikola/spring-boot-single-layer:1 .”
- Push the image on the docker the repository “docker push popovskinikola/spring-boot-single-layer:1”, result from the push image command
From the result, we can see that we are pushing to the docker repo the image layer that was created by the docker file command “COPY ${JAR_FILE} app.jar”. This layer contains the whole fat jar file.
Single Layer Docker File
FROM openjdk:8-jdk-alpin
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Next, change part of the code in one class in our application and do the building process all over again (build the app, then the image, and push to docker repo), you will see that again we are pushing the image layer that contains the whole fat file (16MB) and the result will be the same as the result shown in the picture above.
This can become a problem if we have big fat jars (for example 100MB) and a large number of applications that are getting changed on a daily basis. This process uses a lot of bandwidth, the process is slow and takes a lot of space.
Now the question is, is there a better process where we can push layer changes related to the files that we are changing and not push the whole image layer containing the fat jar?
And the answer is yes, there is a better way, introducing Spring Boot Docker Layers solution.
Multilayer Approach
Support for Docker Layers is a new feature found in Spring Boot 2.3.0. To enable the packaging of layers in the Maven build process, add the following configuration to your Maven POM. An example of the code related to the multilayer approach can be found on Github repo https://github.com/popovski/spring-boot-docker-multi-layer.
How the maven plugin looks like:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
After this setup, if we execute “./mvnw clean package” maven will still create a fat jar, with one difference, inside the jar in folder “BOOT-INF” there will be a file “layers.idx”
- "dependencies"
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
if you want to extract the layers you can use this command “java -Djarmode=layertools -jar my-app.jar extract”. We will use create multi-stage Docker build then we need a different Docker file.
FROM openjdk:11-jre as builde
# set the working directory
WORKDIR application
# copy the jar file
ARG ARTIFACT_NAME=target/*.jar
COPY ${ARTIFACT_NAME} app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM adoptopenjdk:11-jre-hotspot
ARG EXPOSED_PORT
EXPOSE ${EXPOSED_PORT}
ENV SPRING_PROFILES_ACTIVE docker
# we are extracting the jar file so docker can build layers
# every copy command is creating new layer in the image, this can be reviewed using the docker history
# if in some of this layers have change some file then docker will know that
# based on the layers.idx file in the jar file we are doing copy paste
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"]r
In this docker file, you can see that we are doing a couple of stuff. First, we extract the jar layers in a docker image called builder, then we copy the layers in a different image. Every copy command will create a different layer in our image. Based on this docker, image layers docker will know if there are some changes and based on that push only the changes related to some specific layer.
Let’s create a new image that will contain and run our jar file
- Clone the project Git Clone “git@github.com:popovski/spring-boot-docker-multi-layer.git”
- Build the jar file, execute the command in the root directory of the project – “./mvnw clean package”
- The fat jar file can be found under the target folder
- Create the Docker image based on the docker file (see for sample below), using the command “docker build -t popovskinikola/spring-boot-multi-layer:1 .”
- Push the image on the docker the repository “docker push popovskinikola/spring-boot-multi-layer:1”, result from the push image command
- Make some changes in the code, and repeat the process, the result should be similar to the picture below
Conclusion
From the multi-layered result, we can see that separating the fat jar file into multiple layers gives us the possibility to push only the layer that contains the changed files.
The multi-layered approach gives us way better results and flexibility compared to the previous solution where we always push the whole image layer that contains the fat jar.
Our main benefits are:
- faster image creation
- faster development, push and pull docker process is very quick
- we use smaller bandwidth
- our cd/ci pipelines are more optimized
- save on disk space
- the solution is very simple to implement and understand.