Dockerizing a Spring Boot Application

Overview

In this article, we'll cover the process of creating a Docker image of a Spring Boot application, using Dockerfile and Maven and then run the image we've created.

The source code for this tutorial can be found on Github.

This tutorial assumes that you have Docker installed on your machine. If not, you can follow the official Docker install guide based on your operating system.

If you'd like to read more about Docker, we've covered it in detail in - Docker: A High Level Introduction.

The Spring Boot Application

Let's start off with a simple Spring Boot Application. The best way to start with a skeleton project is to visit Spring Initializr. Select your preferred version of Spring Boot, and add the "Web" dependency. Generate it as a Maven project and you're all set!

Spring Initializr

The project includes a simple REST controller with a single mapping that just expects a name as the path variable and then generates a string to greet it:

@RestController
public class DemoController {

  @GetMapping("/greet/{name}")
  public String greeting(@PathVariable String name) {
    return "Hi!! " + name;
  }
}

To run the application, use the following Maven command from the project root folder:

$ mvn spring-boot:run

As usual, the application will be running on port 8080. To test this endpoint, navigate your browser (or use curl, postman, etc.) to "http://localhost:8080/greet/john", you will see a response that looks something like:

REST response 1

Dockerizing the Spring Boot App

Now let's dockerize our previously made Spring Boot Application. We will cover the two most commonly used approaches:

  • Dockerfile – Specifying a file that contains native Docker commands to build the image
  • Maven – Using a Maven plugin to build the image

Dockerizing using Dockerfile

A Dockerfile is just a regular .txt file that includes native Docker commands that are used to specify the layers of an image. To do so, let's create a text file named "Dockerfile":

Docker file

The content of the file itself can look something like this:

FROM java:8-jdk-alpine

COPY ./target/demo-docker-0.0.1-SNAPSHOT.jar /usr/app/

WORKDIR /usr/app

RUN sh -c 'touch demo-docker-0.0.1-SNAPSHOT.jar'

ENTRYPOINT ["java","-jar","demo-docker-0.0.1-SNAPSHOT.jar"]  

Let's take a look at the commands and fully understand them before proceeding:

  • FROM – The keyword FROM tells Docker to use a given base image as a build base. We have used 'java' with tag '8-jdk-alpine'. Think of a tag as a version. The base image changes from project to project. You can search for images on docker-hub.
  • COPY - This tells Docker to copy files from the local file-system to a specific folder inside the build image. Here, we copy our .jar file to the build image (Linux image) inside /usr/app.
  • WORKDIR - The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow in the Dockerfile. Here we switched the workdir to /usr/app so as we don't have to write the long path again and again.
  • RUN - This tells Docker to execute a shell command-line within the target system. Here we practically just "touch" our file so that it has its modification time updated (Docker creates all container files in an "unmodified" state by default).
  • ENTRYPOINT - This allows you to configure a container that will run as an executable. It's where you tell Docker how to run your application. We know we run our spring-boot app as java -jar <app-name>.jar, so we put it in an array.

More documentation can be found on the Dockerfile reference page.

Before moving further, we need a Spring Boot .jar file. This file will be used to create the Docker image as mentioned above.

Run the mvn clean install command to make sure that it's generated.

Let's build the image using this Dockerfile. To do so, move to the root directory of the application and run this command:

$ docker build -t greeting-app .

Docker build 1

We built the image using docker build. We gave it a name with the -t flag and specified the current directory where the Dockerfile is. The image is built and stored in our local docker registry.

Let's check our image:

$ docker images

Docker images 1

And finally, let's run our image:

$ docker run -p 8090:8080 greeting-app 

Docker run 1

We can run Docker images using the docker run command.

We know that each container is an isolated environment in itself and we have to map the port of the host operating system - 8090 and the port inside the container - 8080, which is specified as the -p 8090:8080 argument.

Now, we can access the endpoint on http://localhost:8080/greet/john:

REST response 2

Our Spring Boot application is successfully running within a Docker container!

Dockerizing using Maven

In the previous section we wrote a simple Dockerfile and build our application using the native docker build command. However, there are a couple of issues that we may encounter in our projects using this method:

  • The .jar name - We have to mention the jar name (along with the version) in the file. As our application grows our versions will change and we have to, again and again, update this Dockerfile too.
  • Using the terminal - We have to manually open a terminal and run Docker commands. It would be nice if we could make it a part of a Maven life-cycle so that we can build images as a part of our CI/CD (Continous Integration/Continous Delivery) pipelines.

There are many Maven plugins available that we can use in our pom.xml file that would make our life much easier. The way that this Maven plugin works is that it internally creates the Dockerfile based on the configuration in the pom.xml file and then uses the generated Dockerfile to build the image.

Using this method, there's no need for us to manually update the name, nor run the terminal.

We will be using the fabric8io/docker-maven-plugin.

The plugin should be located in our pom.xml file after the build tag. This will be an optional build plugin using Maven profiles. It's always a good idea to use this via profiles because we want the regular mvn clean install command to work on a developer's machine, which doesn't have Docker installed too:

<profiles>  
   <profile>
      <activation>
         <property>
            <name>docker</name>
         </property>
      </activation>
      <build>
         <plugins>
            <plugin>
               <groupId>io.fabric8</groupId>
               <artifactId>docker-maven-plugin</artifactId>
               <version>0.26.0</version>
               <extensions>true</extensions>
               <configuration>
                  <verbose>true</verbose>
                  <images>
                     <image>
                        <name>${project.artifactId}</name>
                        <build>
                           <from>java:8-jdk-alpine</from>
                           <entryPoint>
                              <exec>
                                 <args>java</args>
                                 <args>-jar</args>
                                 <args>/maven/${project.artifactId}-${project.version}.jar</args>
                              </exec>
                           </entryPoint>
                           <assembly>
                              <descriptorRef>artifact</descriptorRef>
                           </assembly>
                        </build>
                     </image>
                  </images>
               </configuration>
               <executions>
                  <execution>
                     <id>build</id>
                     <phase>post-integration-test</phase>
                     <goals>
                        <goal>build</goal>
                     </goals>
                  </execution>
               </executions>
            </plugin>
         </plugins>
      </build>
   </profile>
</profiles>  

Let's take a closer look at this:

  • Our profile is named docker - If we have to build the image using Maven, we should run the command with -Ddocker.
  • The <name> tag - This tag specifies the image name, which is the artifactId - in our case, it's demo-docker.
  • The <from> tag - This tag specifies the base image of java:8-jdk-alpine.
  • The <args> tag - This tag is used to specify how the image should run.

Now let's build the image:

$ mvn clean install -Ddocker 

Docker build 2

Let's check our image:

$ docker images

Docker images 2

Finally, we'll run our image:

$ docker run -p 8090:8080 demo-docker 

Now check the REST endpoint (http://localhost:8090/greet/john) in the browser.

Note: You have to stop the previously running containers that are using port 8090, or else you will get an error.

You can easily check what containers are running using:

$ docker ps 

Now, if the port is already in use, you can either change the port or remove the old container using:

$ docker rm -f <container-id>

Another easy way is to stop all of the containers:

 $ docker rm -f $(docker ps -a -q) 

Conclusion

In this article, we covered the two most commonly used ways to containerize a Spring Boot application using Docker.

The first approach was done by using a simple Dockerfile to build the image, and the second approach is using a Maven plugin.

Once the image is created, there are many ways to run it. In this article, we relied on the native docker run command. A more approachable way is to use docker-compose if you have many images that you need to run.

For a production environment we usually go for Kubernetes or Docker-swarm so as to scale our application based on network traffic automatically.