As more organizations move to containers and virtual servers, Docker is becoming a more important part of the software development workflow. To that end, among the latest features in Spring Boot 2.3 is the ability to create Docker images for Spring Boot applications.

The purpose of this article is to show you how to create a Docker image for a Spring Boot application.

1. Traditional Docker builds

The traditional way to build a Docker image using Spring Boot is to use a Dockerfile. Here’s a simple example:

FROM OpenJDK: 8-JDK -alpine EXPOSE 8080 ARG JAR_FILE=target/demo-app-1.0.0.jar ADD ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]Copy the code

We can then use the Docker build command to create the Docker image. This is great for most applications, but there are some downsides.

First, we use fat Jars created by Spring Boot. This can affect startup time, especially in a container environment. We can save startup time by adding the decomposing contents of the JAR file.

Second, Docker images are built in layers. Spring Boot Fat JAR features all application code and third-party libraries in one layer. This means that even if only one line of code changes, the entire layer must be rebuilt.

By breaking up the JAR before building, the application code and third-party libraries each get their own layer. In this way, we can take advantage of Docker’s caching mechanism. Now, when a line of code is changed, you just need to rebuild the corresponding layer.

With that in mind, let’s take a look at how Spring Boot can improve the process of creating Docker images.

2. Buildpacks

BuildPacks is a tool that provides framework and application dependencies.

For example, given a Spring Boot FAT JAR, a BuildPack will give us the Java runtime. This allows us to skip the Dockerfile and automatically get a reasonable Docker image.

Spring Boot includes Maven and Gradle support for Bulidpacks. For example, when building with Maven, we would run the following command:

./mvnw spring-boot:build-image
Copy the code

Let’s look at some of the relevant outputs and see what happens:

[INFO] Building jar: target/demo-0.0.1- snapshot.jar... [the INFO] Building image 'docker. IO/library/demo: 0.0.1 - the SNAPSHOT'... [INFO] > Pulling Builder image 'gcr. IO/Paketo-buildPacks/Builder: Base-platform-api-0.3 '100%... [INFO] [creator] ===> DETECTING [INFO] [creator] 5 of 15 buildpacks participating [INFO] [creator] Paketo-buildpacks/Builder-Liberica 2.8.1 [INFO] [Creator] Paketo-buildPacks/Executable jar 1.2.8 [INFO] Paketo-buildpacks /apache-tomcat 1.3.1 [INFO] [creator] Paketo-buildPacks /dist-zip 1.3.6 [INFO] [creator] Paketo - buildpacks/spring - the boot 1.9.1... [the INFO] Successfully built image 'docker. IO/library/demo: 0.0.1 - the SNAPSHOT' [INFO] Total time: 44.796 sCopy the code

The first line shows that we built a standard FAT JAR, just like any other typical Maven package.

The next line starts Docker image building. Then, you see that the Bulid pulls the Packeto builder.

Packeto is based on an implementation of cloud-native BulidPacks. It is responsible for analyzing our project and identifying the required frameworks and libraries. In our case, it determines that we have a Spring Boot project and adds the required build packages.

Finally, we see the generated Docker image and the total build time. Note that during the first build, it took quite a bit of time to download the build pack and create the different layers.

One of the great features of BuildPacks is that Docker images are multi-layered. Therefore, if we just change the application code, subsequent builds will be faster:

. [INFO] [creator] Reusing layer 'paketo-buildpacks/executable-jar:class-path' [INFO] [creator] Reusing layer 'paketo-buildpacks/spring-boot:web-application-type' ... [the INFO] Successfully built image 'docker. IO/library/demo: 0.0.1 - the SNAPSHOT'... [INFO] Total time: 10.591sCopy the code

3. Hierarchical JAR packages

In some cases, we might not like using Bulidpacks — maybe our infrastructure is already tied to another tool, or we already have custom Dockerfiles we want to reuse.

For these reasons, Spring Boot also supports building Docker images using layered jars. To see how it works, let’s look at a typical Spring Boot Fat JAR layout:

org/
  springframework/
    boot/
  loader/
...
BOOT-INF/
  classes/
...
lib/
...
Copy the code

The FAT JAR consists of three main areas:

  • The bootstrap classes required to launch the Spring application
  • Application code
  • Third-party libraries

Using the layered JAR, the structure looks similar, but we get a new layers.idx that maps each directory in the FAT JAR to a layer of files:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"
Copy the code

Out-of-the-box, Spring Boot provides four layers:

Out of the box, Spring Boot provides four layers:

  • dependencies: Dependencies from third parties
  • snapshot-dependencies: Snapshot dependencies from third parties
  • resourcesStatic resource
  • application: Application code and Resources

Our goal is to place application code and third-party libraries into the layer to reflect how often they change.

For example, application code is probably the code that changes the most frequently, so it has its own layer. In addition, each layer can evolve independently, and only when a layer changes will the Docker image be rebuilt for it.

Now that we know about the layered JAR structure, let’s look at how you can leverage it to make Docker images.

3.1. Create layered JARS

First, we must set up a project to create a tiered JAR. For Maven, you need to add a new configuration in the Spring Boot Plugin section of the POM:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layers>
            <enabled>true</enabled>
        </layers>
    </configuration>
</plugin>
Copy the code

With this configuration, the Maven package command (along with its other dependent commands) will generate a new layered JAR using the four default layers mentioned earlier.

3.2. View and extract layers

Next, we need to extract the layers from the JAR so that the Docker image has the correct layers. To examine any layer of the layered JAR, run the following command:

Java -djarmode = layerTools -jar demo-0.0.1.jar listCopy the code

Then extract them and run the command:

Java-djarmode = layerTools -jar demo-0.0.1.jar extractCopy the code

3.3. Create a Docker image

The easiest way to incorporate these layers into a Docker image is to use a Dockerfile:

FROM adoptopenjdk:11-jre-hotspot as builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE}application.jar RUN java -Djarmode=layertools -jar application.jar extract FROM adoptopenjdk:11-jre-hotspot COPY --from=builder dependencies/ ./ COPY --from=builder snapshot-dependencies/ ./ COPY --from=builder spring-boot-loader/ ./  COPY --from=builder application/ ./ ENTRYPOINT ["java"."org.springframework.boot.loader.JarLauncher"]
Copy the code

This Dockerfile extracts layers from the Fat JAR and copies each layer into the Docker image.

Each COPY instruction eventually generates a new layer in the Docker image.

If we build the Dockerfile, we can see that each layer in the layered JAR is added as its own layer to the Docker image:

. Step 6/10 : COPY --from=builder dependencies/ ./ ---> 2c631b8f9993 Step 7/10 : COPY --from=builder snapshot-dependencies/ ./ ---> 26e8ceb86b7d Step 8/10 : COPY --from=builder spring-boot-loader/ ./ ---> 6dd9eaddad7f Step 9/10 : COPY --from=builder application/ ./ ---> dc80cc00a655 ...Copy the code

4. To summarize

In this article, we learned various ways to build Docker images using Spring Boot.

With BuildPacks, we can get proper Docker images without templates or custom configurations.

Or, with a little more effort, we could use a layered JAR to get a more customized Docker image.