Creating Optimized Docker Images for a Spring Boot Application

Containers have become the preferred means of packaging applications with all software and operating system dependencies and then delivering them to various environments.

This article discusses various ways to containerize a Spring Boot application:

  • creating a Docker image using a Dockerfile,
  • creating an OCI image from source code using Cloud-Native Buildpack,
  • and optimizing the image at runtime by breaking the JAR parts into different layers using multi-stage tools.

 Code Example

This article is accompanied by a working code example on GitHub .

Container Terminology

We will start with the container terminology used in this article:

  • Container Image: a file of a specific format. We convert our application into a container image by running a build tool.
  • Container: an executable instance of a container image.
  • Container Engine: a daemon process responsible for running containers.
  • Container Host: the host computer on which the container engine runs.
  • Container Registry: a common location used to publish and distribute container images.
  • OCI StandardOpen Container Initiative (OCI) is a lightweight open governance structure formed under the Linux Foundation. The OCI image specification defines industry standards for container image formats and runtimes, ensuring that all container engines can run container images created by any build tool.

To place an application in a container, we package our application into a container image and publish this image to a common registry. The container runtime fetches this image from the registry, unpacks it, and runs the application inside.

Spring Boot 2.3 provides plugins for creating OCI images.

Docker — the most commonly used container implementation, and we use Docker in our examples, so all subsequent references to containers in this article will mean Docker.

Building a Container Image the Traditional Way

Creating Docker images for Spring Boot applications is very straightforward by adding a few instructions in the Dockerfile.

First, we create a JAR executable file and, as part of the Docker file instructions, copy the JAR executable over the base JRE image after applying the necessary settings.

Let's create our Spring application on Spring Initializr with dependencies weblombokand actuator. We also add a REST controller to provide the API with GETmethod.

Creating a Docker file

Next, we place this application into a container, adding Dockerfile:

FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/application.jar"]

Our Docker file contains a base image from adoptopenjdk, over which we copy our JAR file, and then open the port 8080that will listen for requests.

Building the application

First, we need to create the application using Maven or Gradle. Here, we use Maven:

mvn clean package

This creates the executable JAR file of the application. We need to convert this executable JAR into a Docker image to work in the Docker engine.

Creating a container image

Then we place this executable JAR into the Docker image by executing the command docker buildfrom the root project directory that contains the previously created Docker file:

docker build -t usersignup:v1 .

We can see our image in the list with the command:

docker images 

The result of executing the above command includes our image usersignupalong with the base image adoptopenjdk, specified in our Docker file.

REPOSITORY          TAG                 SIZE
usersignup          v1                  249MB
adoptopenjdk        11-jre-hotspot      229MB

Viewing the layers inside the container image

Let's take a look at the stack of layers inside the image. We'll use tool  dive, to view these layers:

dive usersignup:v1

Here is part of the results from running the Dive command: 

Creating Optimized Docker Images for a Spring Boot Application

As we can see, the application layer makes up a significant portion of the image size. We want to reduce the size of this layer in the following sections as part of our optimization.

Creating a container image using Buildpack

Buildpacks (Buildpacks) is a general term used by various Platform as a Service (PAAS) offerings to create a container image from source code. It was launched by Heroku in 2011 and has since been adopted by Cloud Foundry, Google App Engine, Gitlab, Knative, and some others.

Creating Optimized Docker Images for a Spring Boot Application

The advantage of cloud buildpacks

One of the main advantages of using Buildpacks to create images is that Image configuration changes can be managed centrally (builder) and distributed to all applications using the builder.

Buildpacks were tightly linked to the platform. Cloud-Native Buildpacks provide standardization across platforms by supporting the OCI image format, ensuring that the image can run on the Docker engine.

Using the Spring Boot plugin

The Spring Boot plugin creates OCI images from source code using Buildpack. Images are created using bootBuildImagetasks (Gradle) or spring-boot:build-imagegoals (Maven) and local Docker installation.

We can configure the image name needed for pushing to the Docker registry by specifying the name in the image tag:

org.springframework.boot
  spring-boot-maven-plugin
  
    
      docker.io/pratikdas/${project.artifactId}:v1

Let's use Maven to execute the build-imagegoal to create the application and generate the container image. We are currently not using any Docker files.

mvn spring-boot:build-image

The result will look something like this:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Building image 'docker.io/pratikdas/usersignup:v1'
[INFO] 
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator]     Adding label 'org.springframework.boot.version'
.. [creator]     *** Images (c311fe74ec73):
.. [creator]           docker.io/pratikdas/usersignup:v1
[INFO] 
[INFO] Successfully built image 'docker.io/pratikdas/usersignup:v1'

From the output, we can see that paketo Cloud-Native buildpackis used to create a working OCI image. As before, we can see the image specified as a Docker image by running the command:

docker images 

Output:

REPOSITORY                             SIZE
paketobuildpacks/run                  84.3MB
gcr.io/paketo-buildpacks/builder      652MB
pratikdas/usersignup                  257MB

Creating a container image using Jib

Jib is an image creation plugin from Google that provides an alternative method for building a container image from source code.

Configuring jib-maven-pluginin pom.xml:

      com.google.cloud.tools
        jib-maven-plugin
        2.5.2

Next, we run the Jib plugin using Maven command to build the application and create the container image. As before, we are not using any Docker files here:

mvn compile jib:build -Dimage=/usersignup:v1

After executing the above Maven command, we get the following output:

[INFO] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete

The output indicates that the container image has been created and pushed to the registry.

Motivations and methods for creating optimized images

We have two main reasons for optimization:

  • Performance: in the container orchestration system, the container image is pulled from the image registry to the host running the container engine. This process is called scheduling. Pulling large images from the registry leads to long scheduling times in container orchestration systems and extended build times in CI pipelines.
  • Security: large images also present a greater attack surface.

A Docker image consists of a stack of layers, each representing an instruction in our Dockerfile. Each layer represents a delta of changes from the underlying layer. When we pull a Docker image from the registry, it is pulled in layers and cached on the host.

Spring Boot uses the "fat JAR" as the default packaging format. When we look at the fat JAR, we see that the application constitutes a very small part of the entire JAR. This part changes the most frequently. The remaining part consists of Spring Framework dependencies. The optimization formula focuses on isolating the application onto a separate layer from the Spring Framework dependencies.

The dependency layer, making up the bulk of the fat JAR file, is loaded only once and cached in the host system.

Only the thin application layer is pulled during application updates and container scheduling,

as illustrated in this diagram: In the following sections, we will discuss how to create these optimized images for a Spring Boot application.

Creating Optimized Docker Images for a Spring Boot Application

Creating an optimized container image for a Spring Boot application using Buildpack

Spring Boot 2.3 supports multi-layering by extracting parts of the fat JAR file into separate layers. The layering feature is disabled by default and must be explicitly enabled using the Spring Boot Maven plugin:

Spring Boot 2.3 supports multilayering by extracting parts of a thick JAR file into separate layers. The layering feature is disabled by default and must be explicitly enabled using the Spring Boot Maven plugin:

org.springframework.boot
  spring-boot-maven-plugin
  
    
      true

We will use this configuration to create our container image first with Buildpack and then with Docker in the following sections.

Let's run build-imagethe Maven goal to create a container image:

mvn spring-boot:build-image

If we run Dive to see the layers in the resulting image, we will see that the application layer (highlighted in red) is significantly smaller in the kilobyte range compared to what we obtained using the fat JAR format:

Creating Optimized Docker Images for a Spring Boot Application

Creating an optimized container image for a Spring Boot application using Docker

Instead of using the Maven or Gradle plugin, we can also create a multi-layer Docker image with a Docker file.

When we use Docker, we need to perform two additional steps to extract the layers and copy them into the final image.

The contents of the resulting JAR after building with Maven with layering enabled will look as follows:

META-INF/
.
BOOT-INF/lib/
.
BOOT-INF/lib/spring-boot-jarmode-layertools-2.3.3.RELEASE.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx

The output shows an additional JAR named spring-boot-jarmode-layertoolsand layersfle.idxfile. This additional JAR file provides multi-layer processing capabilities as described in the next section.

Extracting dependencies in separate layers

To view and extract layers from our multi-layer JAR, we use the system property -Djarmode=layertoolsto run spring-boot-jarmode-layertoolsthe JAR instead of the application:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar

Executing this command yields output containing the available command options:

Usage:
  java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command

The output shows the commands listextractand helpwith helpto be default. Let's run the command with listthe option:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

We see a list of dependencies that can be added as layers.

Default layers:

Layer Name

Content

dependencies

any dependency whose version does not contain SNAPSHOT

spring-boot-loader

JAR loader classes

snapshot-dependencies

any dependency whose version contains SNAPSHOT

application

application classes and resources

Layers are defined in layers.idxthe file in the order they should be added to the Docker image. These layers are cached on the host after the first fetch, as they do not change. Only the updated application layer is uploaded to the host, which occurs faster due to the reduced size. .

Building the image with dependencies fetched in separate layers

We will build the final image in two stages using a method called multi-stage builds . In the first stage, we will extract the dependencies, and in the second stage, we will copy the extracted dependencies into the final image.

Let’s modify our Docker file for multi-stage builds:

# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-jre-hotspot
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"]

We save this configuration in a separate file — Dockerfile2.

We build the Docker image with the command:

docker build -f Dockerfile2 -t usersignup:v1 .

After executing this command, we get the following output:

Sending build context to Docker daemon  20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1

We see that the Docker image is created with an image ID, and then tagged.

Finally, we run the Dive command as before to inspect the layers inside the generated Docker image. We can specify the image ID or tag as input to the Dive command:

dive userssignup:v1

As seen from the output, the layer containing the application now takes up only 11 KB, and the dependencies are cached in separate layers. 

Creating Optimized Docker Images for a Spring Boot Application

Fetching internal dependencies in separate layers

We can further reduce the size of the application layer by extracting any of our custom dependencies into a separate layer instead of bundling them with the application, declaring them in ymla file named layers.idx:

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

In this file layers.idxwe added a custom dependency named, io.myorgcontaining organization dependencies retrieved from a shared repository.

Output

In this article, we explored the use of Cloud-Native Buildpacks to create a container image directly from source code. This is an alternative to using Docker to create a container image in the traditional way: first, a thick executable JAR file is created, and then it is packaged into a container image by specifying instructions in a Docker file.

We also looked at optimizing our container by including the layering feature, which extracts dependencies into separate layers that are cached on the host, while the thin application layer is loaded at runtime in the container execution mechanisms.

You can find all the source code used in the article at Github .

Command reference

Here’s a brief summary of the commands we used in this article for quick reference.

Cleanup context:

docker system prune -a

Building a container image with a Docker file:

docker build -f  -t  .

Building a container image from source code (without Dockerfile):

mvn spring-boot:build-image

Viewing dependency layers. Before building the application JAR file, ensure that the layering feature is enabled in spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Extracting dependency layers. Before building the application JAR file, ensure that the layering feature is enabled in spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Viewing the list of container images

docker images

Viewing layers within the container image (ensure that the dive tool is installed):

dive

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster