DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Using KRaft Kafka for Development and Kubernetes Deployment
  • Setting Up Local Kafka Container for Spring Boot Application
  • Integrating Spring Boot Microservices With MySQL Container Using Docker Desktop
  • Keep Your Application Secrets Secret

Trending

  • Beyond ChatGPT, AI Reasoning 2.0: Engineering AI Models With Human-Like Reasoning
  • Unlocking the Potential of Apache Iceberg: A Comprehensive Analysis
  • Create Your Own AI-Powered Virtual Tutor: An Easy Tutorial
  • Mastering Fluent Bit: Installing and Configuring Fluent Bit on Kubernetes (Part 3)
  1. DZone
  2. Software Design and Architecture
  3. Microservices
  4. Spring Boot Docker Best Practices

Spring Boot Docker Best Practices

By 
Gunter Rotsaert user avatar
Gunter Rotsaert
DZone Core CORE ·
Dec. 20, 22 · Tutorial
Likes (12)
Comment
Save
Tweet
Share
8.5K Views

Join the DZone community and get the full member experience.

Join For Free

In this blog, you will learn some Docker best practices mainly focussed on Spring Boot applications. You will learn these practices by applying them to a sample application. Enjoy!

1. Introduction

This blog continues where the previous blog about Docker Best Practices left off. However, this blog can be read independently from the previous one. The goal is to provide some best practices that can be applied to Dockerized Spring Boot applications.

The Dockerfile that will be used as a starting point is the following:

Dockerfile
 
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
WORKDIR /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
ARG JAR_FILE
COPY target/${JAR_FILE} app.jar
RUN chown -R javauser:javauser .
USER javauser
ENTRYPOINT ["java", "-jar", "app.jar"]


This Dockerfile is doing the following:

  • FROM: Take eclipse-temurin:17 Java Docker image as base image;
  • WORKDIR: Set /opt/app as the working directory;
  • RUN: Create a system group and system user;
  • ARG: provide an argument JAR_FILE so that you do not have to hard code the jar file name into the Dockerfile;
  • COPY: Copy the jar file into the Docker image;
  • RUN: Change the owner of the WORKDIR to the previously created system user;
  • USER: Ensure that the previously created system user is used;
  • ENTRYPOINT: Start the Spring Boot application.

In the next sections, you will change this Dockerfile to adhere to best practices. The resulting Dockerfile of each paragraph is available in the git repository in the directory Dockerfiles. At the end of each paragraph, the name of the corresponding final Dockerfile will be mentioned where applicable.

The code being used in this blog is available on GitHub.

2. Prerequisites

The following prerequisites apply to this blog:

  • Basic Linux knowledge
  • Basic Java and Spring Boot knowledge
  • Basic Docker knowledge

3. Sample Application

A sample application is needed in order to demonstrate the best practices. Therefore, a basic Spring Boot application is created containing the Spring Web and Spring Actuator dependencies.

The application can be run by invoking the following command from within the root of the repository: 

Shell
 
$ mvn spring-boot:run


Spring Actuator will provide a health endpoint for your application. By default, it will always return the UP status.

Shell
 
$ curl http://localhost:8080/actuator/health
{"status":"UP"}


In order to alter the health status of the application, a custom health indicator is added. Every 5 invocations, the health of the application will be set to DOWN.

Java
 
@Component
public class DownHealthIndicator implements HealthIndicator {
 
    private int counter;
 
    @Override
    public Health health() {
        counter++;
        Health.Builder status = Health.up();
        if (counter == 5) {
            status = Health.down();
            counter = 0;
        }
        return status.build();
    }
}


For building the Docker image, a fork of the dockerfile-maven-plugin of Spotify will be used. The following snippet is therefore added to the pom file.

XML
 
<plugin>
  <groupId>com.xenoamess.docker</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.25</version>
  <configuration>
    <repository>mydeveloperplanet/dockerbestpractices</repository>
    <tag>${project.version}</tag>
    <buildArgs>
      <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
    </buildArgs>
  </configuration>
</plugin>


The advantage of using this plugin is that you can easily reuse the configuration. Creating the Docker image can be done by a single Maven command.

Building the jar file is done by invoking the following command:

Shell
 
$ mvn clean verify


Building the Docker image can be done by invoking the following command:

Shell
 
$ mvn dockerfile:build


Run the Docker image:

Shell
 
$ docker run --name dockerbestpractices mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT


Find the IP-address of the running container:

Shell
 
$ docker inspect dockerbestpractices | grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.3",
                    "IPAddress": "172.17.0.3"


In the above example, the IP-address is 172.17.0.3.

The application also contains a HelloController which just responds with a hello message. The Hello endpoint can be invoked as follows:

Shell
 
$ curl http://172.17.0.3:8080/hello
Hello Docker!


Everything is now explained to get started!

4. Best Practices

4.1 Healthcheck

A healthcheck can be added to your Dockerfile in order to expose the health of your container. Based on this status, the container can be restarted. This can be done by means of the HEALTHCHECK command. Add the following healthcheck:

Dockerfile
 
HEALTHCHECK --interval=30s --timeout=3s --retries=1 CMD wget -qO- http://localhost:8080/actuator/health/ | grep UP || exit 1


This healthcheck is doing the following:

  • interval: Every 30 seconds the healthcheck is executed. For production use, it is better to choose something like five minutes. In order to do some tests, a smaller value is easier. This way you do not have to wait for five minutes each time.
  • timeout: A timeout of three seconds for executing the health check.
  • retries: This indicates the number of consecutive checks which have to be executed before the health status changes. This defaults to three which is a good number for in-production. For testing purposes, you set it to one, meaning that after one unsuccessful check, the health status changes to unhealthy.
  • command: The Spring Actuator endpoint will be used as a healthcheck. The response is retrieved and piped to grep in order to verify whether the health status is UP. It is advised not to use curl for this purpose because not every image has curl available. You will need to install curl in addition to the image and this enlarges the image with several MBs.

Build and run the container.

Take a closer look at the status of the container. In the first 30 seconds, the health status indicates starting because the first health check will be done after the interval setting.

Shell
 
$ docker ps
CONTAINER ID   IMAGE                                                  COMMAND                  CREATED         STATUS                            PORTS     NAMES
ddffb5a9cbf0   mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT   "java -jar /opt/app/…"   8 seconds ago   Up 6 seconds (health: starting)             dockerbestpractices


After 30 seconds, the health status indicates healthy.

Shell
 
$ docker ps
CONTAINER ID   IMAGE                                                  COMMAND                  CREATED          STATUS                    PORTS     NAMES
ddffb5a9cbf0   mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT   "java -jar /opt/app/…"   33 seconds ago   Up 32 seconds (healthy)             dockerbestpractices


After 2-5 minutes, the health status indicates unhealthy because of the custom health indicator you added to the sample application.

Shell
 
$ docker ps
CONTAINER ID   IMAGE                                                  COMMAND                  CREATED         STATUS                     PORTS     NAMES
ddffb5a9cbf0   mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT   "java -jar /opt/app/…"   2 minutes ago   Up 2 minutes (unhealthy)             dockerbestpractices


Again, 30 seconds after the unhealthy status, the status reports healthy. Did you notice that the container did not restart due to the unhealthy status? That is because the Docker engine does not do anything based on this status. A container orchestrator like Kubernetes will do a restart. Is it not possible to restart the container when running with the Docker engine? Yes, it can: you can use the autoheal Docker image for this purpose. Let’s start the autoheal container.

Shell
 
docker run -d \
    --name autoheal \
    --restart=always \
    -e AUTOHEAL_CONTAINER_LABEL=all \
    -v /var/run/docker.sock:/var/run/docker.sock \
    willfarrell/autoheal


Verify whether it is running.

Shell
 
$ docker ps
CONTAINER ID   IMAGE                                                  COMMAND                  CREATED          STATUS                    PORTS     NAMES
ddffb5a9cbf0   mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT   "java -jar /opt/app/…"   10 minutes ago   Up 10 minutes (healthy)             dockerbestpractices
d40243eb242a   willfarrell/autoheal                                   "/docker-entrypoint …"   5 weeks ago      Up 9 seconds (healthy)              autoheal


Wait until the health is unhealthy again or just invoke the health actuator endpoint in order to speed it up. When the status reports unhealthy, the container is restarted. You can verify this in the STATUS column where you can see the uptime of the container.

Shell
 
$ docker ps
CONTAINER ID   IMAGE                                                  COMMAND                  CREATED          STATUS                            PORTS     NAMES
ddffb5a9cbf0   mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT   "java -jar /opt/app/…"   12 minutes ago   Up 6 seconds (health: starting)             dockerbestpractices


You have to decide for yourself whether you want this or whether you want to monitor the health status yourself by means of a monitoring tool. The autoheal image provides you the means to automatically restart your Docker container(s) without manual intervention.

The resulting Dockerfile is available in the git repository with the name 6-Dockerfile-healthcheck.

4.2 Docker Compose

Docker Compose gives you the opportunity to start multiple containers at once with a single command. Besides that, it also enables you to document your services, even when you only have one service to manage. Docker Compose used to be installed separately from Docker, but nowadays it is part of Docker itself. You need to write a compose.yml file that contains this configuration. Let’s see what this looks like for the two containers you used during the healthcheck.

YAML
 
services:
  dockerbestpractices:
    image: mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT
 
  autoheal:
    image: willfarrell/autoheal:1.2.0
    restart: always
    environment:
      AUTOHEAL_CONTAINER_LABEL: all
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock


Two services (read: containers) are configured. One for the dockerbestpractices image and one for the autoheal image. The autoheal image will restart after a reboot, has an environment variable defined, and has a volume mounted.

Execute the following command from the directory where the compose.yml file can be found:

Shell
 
$ docker compose up


In the logging, you will see that both containers are started. Open another terminal window and navigate to the directory where the compose.yml can be found. A lot of commands can be used in combination with Docker Compose. E.g. show the status of the running containers.

Shell
 
$ docker compose ps
NAME                                                COMMAND                  SERVICE               STATUS              PORTS
mydockerbestpracticesplanet-autoheal-1              "/docker-entrypoint …"   autoheal              running (healthy)   
mydockerbestpracticesplanet-dockerbestpractices-1   "java -jar /opt/app/…"   dockerbestpractices   running (healthy)


Or stop the containers:

Shell
 
$ docker compose stop
[+] Running 2/2
 ⠿ Container mydockerbestpracticesplanet-autoheal-1             Stopped                                                                         4.3s
 ⠿ Container mydockerbestpracticesplanet-dockerbestpractices-1  Stopped                                                                         0.3s


Or easily remove the containers:

Shell
 
$ docker compose rm
? Going to remove mydockerbestpracticesplanet-dockerbestpractices-1, mydockerbestpracticesplanet-autoheal-1 Yes
[+] Running 2/0
 ⠿ Container mydockerbestpracticesplanet-autoheal-1             Removed                                                                                                                                                   0.0s
 ⠿ Container mydockerbestpracticesplanet-dockerbestpractices-1  Removed                                                                                         


As you can see, Docker Compose provides quite some advantages and you should definitely consider using it.

4.3 Multi-Stage Builds

Sometimes it can be handy to build your application inside a Docker container. The advantage is that you do not need to install a complete development environment onto your system and that you can interchange the development environment more easily. However, there is a problem with building the application inside your container. Especially when you want to use the same container for running your application. The sources and the complete development environment will be available in your production container and this is not a good idea from a security perspective. You could write separate Dockerfiles to circumvent this issue: one for the build and one for running the application. But this is quite cumbersome. The solution is to use multi-stage builds. With multi-stage builds, you can separate the building stage from the running stage. The Dockerfile looks as follows:

Dockerfile
 
FROM maven:3.8.6-eclipse-temurin-17-alpine@sha256:e88c1a981319789d0c00cd508af67a9c46524f177ecc66ca37c107d4c371d23b AS builder
WORKDIR /build
COPY . .
RUN mvn clean package -DskipTests
 
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
WORKDIR /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
COPY --from=builder /build/target/mydockerbestpracticesplanet-0.0.1-SNAPSHOT.jar app.jar
RUN chown -R javauser:javauser .
USER javauser
HEALTHCHECK --interval=30s --timeout=3s --retries=1 CMD wget -qO- http://localhost:8080/actuator/health/ | grep UP || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]


As you can see, this Dockerfile contains two FROM statements. The first one is used for building the application:

  • FROM: A Docker image containing Maven and Java 17, this is needed for building the application;
  • WORKDIR: Set the working directory;
  • COPY: copy the current directory to the working directory into the container;
  • RUN: The command in order to build the jar file.

Something else is also added to the FROM statement. At the end, AS builder is added. This way, this container is labeled and can be used for building the image for running the application. The second part is identical to the Dockerfile you used to have before, except for two lines. The following lines are removed:

Dockerfile
 
ARG JAR_FILE
COPY target/${JAR_FILE} app.jar


These lines ensured that the jar file from our local build was copied into the image. These are replaced with the following line:

Dockerfile
 
COPY --from=builder /build/target/mydockerbestpracticesplanet-0.0.1-SNAPSHOT.jar app.jar


With this line, you indicate that you want to copy a file from the builder container into the new image.

When you build this Dockerfile, you will notice that the build container executes the build and finally, the image for running the application is created. During building the image, you will also notice that all Maven dependencies are downloaded.

The resulting Dockerfile is available in the git repository with the name 7-Dockerfile-multi-stage-build.

4.4 Spring Boot Docker Layers

A Docker image consists of layers. If you are not familiar with Docker layers, you can check out a previous post. Every command in a Dockerfile will result in a new layer. When you initially pull a Docker image, all layers will be retrieved and stored. If you update your Docker image and you only change for example the jar file, the other layers will not be retrieved anew. This way, your Docker images are stored more efficiently. However, when you are using Spring Boot, a fat jar is created. Meaning that when you only change some of your code, a new fat jar is created with unchanged dependencies. So each time you create a new Docker image, megabytes are added in a new layer without any necessity. For this purpose, Spring Boot Docker layers can be used. A detailed explanation can be found here. In short, Spring Boot can split the fat jar into several directories:

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

The application code will reside in the directory application, whereas for example, the dependencies will reside in directory dependencies. In order to achieve this, you will use a multi-stage build.

The first part will copy the jar file into a JDK Docker image and will extract the fat jar.

Dockerfile
 
FROM eclipse-temurin:17.0.4.1_1-jre-alpine@sha256:e1506ba20f0cb2af6f23e24c7f8855b417f0b085708acd9b85344a884ba77767 AS builder
WORKDIR application
ARG JAR_FILE
COPY target/${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract


The second part will copy the split directories into a new image. The COPY commands replace the jar file.

Shell
 
FROM eclipse-temurin:17.0.4.1_1-jre-alpine@sha256:e1506ba20f0cb2af6f23e24c7f8855b417f0b085708acd9b85344a884ba77767
WORKDIR /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
RUN chown -R javauser:javauser .
USER javauser
HEALTHCHECK --interval=30s --timeout=3s --retries=1 CMD wget -qO- http://localhost:8080/actuator/health/ | grep UP || exit 1
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]


Build and run the container. You will not notice any difference when running the container. The main advantage is the way the Docker image is stored.

The resulting Dockerfile is available in the git repository with the name 8-Dockerfile-spring-boot-docker-layers.

5. Conclusion

In this blog, some best practices are covered when creating Dockerfiles for Spring Boot applications. Learn to apply these practices and you will end up with much better Docker images.

Docker (software) Spring Boot

Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Using KRaft Kafka for Development and Kubernetes Deployment
  • Setting Up Local Kafka Container for Spring Boot Application
  • Integrating Spring Boot Microservices With MySQL Container Using Docker Desktop
  • Keep Your Application Secrets Secret

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!