Build and Run a Spring Boot Application with Docker

Build and Run a Spring Boot Application with Docker

In this post, we’ll use Docker to create an image of a Spring Boot application and run it in a container. We’ll start with a single Dockerfile, then we’ll also cover how to scale our application using Docker Compose, and we’ll see how to build our Java code using Docker.

Table of Contents

The Hola Spring Boot application

Updated: Code examples use Java 10 and Spring Boot 2. More info.

The application we’ll employ through this post is pretty simple: a Spring Boot Web application with a REST controller giving us a warm Spanish greeting. Apart from the ‘Hola’, we’ll show from which IP address the greeting is coming from. That will be useful for us to see how Docker works.

Docker Spring Boot - tpd.io
All the code in this post is available on GitHub: Hola Docker Spring Boot. If you find it useful, please give it a star!

Creating an example controller

I’ll guide you through this post as if you were creating the application from scratch, but you can also clone the existing repository and play with it.

First, let’s create the application skeleton using the Spring Boot Initializr: http://start.spring.io, including ‘Web’ as the only dependency and choosing Maven (to follow the same instructions as in this post). Then, we code this simple REST Controller:

package com.thepracticaldeveloper.hola;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.InetAddress;
import java.net.UnknownHostException;

@RestController
public final class HolaController {

    @GetMapping
    public final String hola() throws UnknownHostException {
        return "Hola! Puedes encontrarme en " + InetAddress.getLocalHost().getHostAddress();
    }
}

Since there is no specified path, that text is returned when we perform a GET request to the root context. Let’s give it a try.

Running the application

Use your local Maven installation or the wrapper (mvnw command) normally included by the Spring initializer. From the application’s root folder, execute:

mvn spring-boot:run

The Spring Boot application should start and show a message similar to this one:

Started HolaApplication in 2.198 seconds (JVM running for 5.628)

If you navigate with your browser (or use command line tools like curl or httpie) to localhost:8080, you’ll see the response from our friendly app (the IP may vary of course):

Hola! Puedes encontrarme en 192.168.0.21

As an alternative, you can also run the application by first packaging it in a jar file, and then run it with the java command. In that case, use Maven to create the package:

mvn clean package

The resulting .jar file will be placed in a new target folder. Now you can execute this command and you’ll get the application running as before:

java -jar ./target/hola-docker-1.0.0-SNAPSHOT.jar

Understanding the basics

So far there is nothing to do with Docker, but it’s important to highlight a couple of concepts to fully understand the rest of this post:

  • You used your machine to build the application (using Maven, either previously installed or the wrapper).
  • You used your machine to run the application (either using the spring-boot plugin or the jar file).

Dockerizing Spring Boot (or any executable .jar file)

Let’s start playing with Docker. If you haven’t done it yet, you need to install Docker in one of its versions (Windows, Mac, Linux). If you use an old Windows or Mac system, you’ll need to use the Docker Toolbox. In that case, please note that when I refer to localhost, you should replace it with the IP of your VM (in which Docker runs).

To keep the learning path as smooth as possible, I’ll go through several steps that will show you different things you can do with Docker and a java application:

  1. Use a Dockerfile to define an image, then build it and run it with plain Docker commands.
  2. Use a docker-compose.yml and its command line interface to extend the functionalities when running multiple containers, and simplify it by having a predefined configuration.
  3. Use docker to build our java code as well (note that this is more an experiment than a real-life case).
Get the book Practical Software Architecture

Minimal configuration: Dockerfile

Dockerfile for Spring Boot

The minimal requirement we have to run our Spring Boot app in a container is to create a file named Dockerfile with instructions to build the image. Then we’ll run a Docker container using that image.

FROM openjdk:10-jre-slim
COPY ./target/hola-docker-1.0.0-SNAPSHOT.jar /usr/src/hola/
WORKDIR /usr/src/hola
EXPOSE 8080
CMD ["java", "-jar", "hola-docker-1.0.0-SNAPSHOT.jar"]

As you see, I kept it really simple. The image is based on a slim Linux with JRE 10; on top of that we copy the JAR file, we change to the working directory in which that package is, and we execute the same command as we did before when running from our machine. The EXPOSE instruction is telling Docker that the 8080 port can be exposed outside the container, but note that we’ll also need to make the port available when running it anyways. If you want further details about what every line does, I recommend you to have a look at the Dockerfile reference documentation.

If you use Java 8 instead, you can benefit from an even smaller Linux image (alpine). Unfortunately, the alpine distribution is not yet compatible with Java 10 at the time of writing this post.

Building the image

We build the image so it’ll be available in our local Docker registry. We give it a name with the -t flag, and specify the current directory as the one in which the Dockerfile lives.

docker build -t hola-manual-build .

The command will output the status of every step while building the image. It will download the base image if it’s not available yet in your existing docker images, so be patient. If we now run the command docker images to list the available images in our registry, we’ll see the new one:

REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
hola-manual-build              latest              190f9d37da59        7 hours ago         302MB

Running the Spring Boot application with Docker

We’re almost there to have our application up and running on Docker. We just need to create a container using the new image:

docker run -p 8000:8080 hola-manual-build

By using the -p flag we’re telling docker to expose the container’s port 8080 (on the right of the colon) on the host’s port 8000 (on the left, our machine). We can access from our machine to localhost:8000 (you can also use your browser) and see the greeting message again, this time coming from the Docker container:

moises$ http localhost:8000
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 10:05:01 GMT

Hola! Puedes encontrarme en 172.17.0.2

Note that the IP is different from the previous one since now the application is deployed inside a Docker container. Each container will get a new assigned IP inside the Docker’s network.

Dockerfile: Recap

We did it! We have now the java Spring Boot application running in a Docker container. Note the difference with the previous case:

  • You used your machine to build the application (using Maven, either previously installed or the wrapper).
  • You used Docker to run the application:
    • You built the image using docker build.
    • You ran the container using docker run, specifying the port to make it available from the host.

Running a Spring Boot application using docker-compose

Docker Compose is a tool to run multiple containers, define how they are connected, how many instances should be deployed, etc. It allows you to define how the image should be built as well. Let’s use it in our scenario to simplify the way we run the container.

Defining the docker-compose file

We create a file docker-compose.yml with this content:

version: '2.2'
services:
  hola:
    build:
      context: ./
      dockerfile: Dockerfile
    image: holaweb
    ports:
      - 8080
    networks:
      - network1

networks:
  network1:

We create only one service: hola. We define how it’s built: using the same folder as a context, since there is the Dockerfile and the different files located. We don’t need to specify the dockerfile parameter, but we’ll introduce it here since we plan to modify it later. We give a name to the image so, if it’s not present, the compose tool will build it and assign that name to the resulting image. If it’s there, it won’t build the image again unless we instruct it to do so (we’ll see how later).

Building and Running the application

Note that we’re using ports to make the container port 8080 available from outside. In this case, we’re not specifying the host port, so Docker will pick a random one. Let’s execute docker-compose and see what happens:

$ docker-compose up

This should be the output of the command:

$ docker-compose up
Building hola
Step 1/5 : FROM openjdk:10-jre-slim
 ---> 65f8e952b8a9
Step 2/5 : COPY ./target/hola-docker-1.0.0-SNAPSHOT.jar /usr/src/hola/
 ---> 4b9605f98946
Step 3/5 : WORKDIR /usr/src/hola
Removing intermediate container 4a9ec666872f
 ---> 0e487bfb3314
Step 4/5 : EXPOSE 8080
 ---> Running in 4765084c3829
Removing intermediate container 4765084c3829
 ---> 92e311cc1504
Step 5/5 : CMD ["java", "-jar", "hola-docker-1.0.0-SNAPSHOT.jar"]
 ---> Running in c2a9cd73bb8b
Removing intermediate container c2a9cd73bb8b
 ---> ada28f26d28a
Successfully built ada28f26d28a
Successfully tagged holaweb:latest
WARNING: Image for service hola was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating spring-boot-hola-docker_hola_1 ... done
Attaching to spring-boot-hola-docker_hola_1
hola_1  |
hola_1  |   .   ____          _            __ _ _
hola_1  |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
hola_1  | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
hola_1  |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
hola_1  |   '  |____| .__|_| |_|_| |_\__, | / / / /
hola_1  |  =========|_|==============|___/=/_/_/_/
hola_1  |  :: Spring Boot ::        (v2.0.3.RELEASE)
hola_1  |
hola_1  | [Application logs...]

As you can see, the first step in docker-compose builds the image, since there is no holaweb image available. It will tag it with that name, and then run it according to the rest of the details in the docker-compose.yml file. It will expose the container’s port to the host in a port that we don’t know yet. Let’s find it out (you need to open a new terminal):

$ docker-compose ps

And here is the output of that command:

   Name                  Command               State            Ports
------------------------------------------------------------------------------
hola_hola_1   java -jar hola-docker-1.0. ...   Up      0.0.0.0:32768->8080/tcp

So now we know that the host port is 32768 (it may vary in your case). We navigate to localhost:32768 to have our greeting, again coming from a container.

Hola! Puedes encontrarme en 172.20.0.2

These are some remarks about this solution:

  • Subsequent executions of docker-compose up won't require building the image.
  • However, you can rebuild the image whenever you want, by using:
    • docker-compose build, which will build all the services but not run them.
    • docker-compose up --build, which will build all the services and then run them.

Docker-compose: Recap

Let’s summarize what we got so far with this docker-compose solution:

  • You used your machine to build the application (using Maven, either previously installed or the wrapper).
  • You used docker-compose to build and run the application.
    • You built and run the container in one command, using docker-compose up

Scaling up the Spring Boot app using docker-compose

Changes in docker-compose

We can use docker-compose to run multiple container instances of the same image. We prepared for that already, by leaving Docker to assign a random port in the host for our application, so we only need to add one line to our docker-compose.yml file to run multiple instances (in this case 3):

version: '2.2'
services:
  hola:
    build:
      context: ./
      dockerfile: Dockerfile
    image: holaweb
    ports:
      - 8080
    networks:
      - network1
    scale: 3

networks:
  network1:

Just by adding that scale: 3 line, we’ll get three containers up and running when we run:

$ docker-compose up

Listing the multiple Docker instances

You’ll see in the output how the three containers with the three Spring Boot applications start in parallel. In a new terminal, let’s now execute:

$ docker-compose ps

So we can list what are the ports exposed from Docker. In my case, this is the output:

$ docker-compose ps
   Name                  Command               State            Ports
------------------------------------------------------------------------------
hola_hola_1   java -jar hola-docker-1.0. ...   Up      0.0.0.0:32774->8080/tcp
hola_hola_2   java -jar hola-docker-1.0. ...   Up      0.0.0.0:32773->8080/tcp
hola_hola_3   java -jar hola-docker-1.0. ...   Up      0.0.0.0:32775->8080/tcp

You can see how easy is to have multiple instances of our containers up and running. You can also override the number of instances from the command line, by running docker-compose up --scale holaweb=[number_of_instances].

Testing

If we do the GET requests to the three different ports, we’ll see how each container responds with a different IP in their greeting:

moises$ http localhost:32774
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 11:11:34 GMT

Hola! Puedes encontrarme en 172.20.0.3

moises$ http localhost:32773
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 11:12:08 GMT

Hola! Puedes encontrarme en 172.20.0.2

moises$ http localhost:32775
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 11:12:11 GMT

Hola! Puedes encontrarme en 172.20.0.4

If you’re experienced with topics such as service discovery and routing, you must be visualizing now what are the possibilities that docker brings. We could include in this scenario a service registry and an API gateway and benefit from load balancing without too much effort. I’ll try to cover that in a different post.

My book can help you to grasp these patterns - Service Discovery and Routing

Building a Spring Boot app using Docker

Introduction

This is a more advanced scenario in which we use a docker container to build the application and also to run it. Don’t do this unless you have a good reason; most of the modern Continuous Integration tools use Docker themselves to create dynamic containers to build the application packages, run the tests, etc.

Anyway, I wanted to cover this since it’s a good showcase of the possibilities of Docker and it can make sense in some specific situations:

  • You want to build locally but you're working on a machine that doesn't fit the requirements to build the application. For example, you need linux / mac tools but you're running on windows.
  • Your application requires a complex setup to be built so it's convenient to have a pre-cooked docker image with all the required packages and configuration that you can run anywhere.

However, note that for the scenario shown here it’s easier to build the code as we’ve been doing until now, just using java and maven on our machine while building the application and then docker to run it. Anyway, we’ll use it as a reference so you can get the idea and apply it (if needed) to other cases.

Get the book Practical Software Architecture

Dockerfile to build Java code and run it

Let’s create a different Dockerfile, which we’ll name Dockerfile-build:

FROM maven:3.5.4-jdk-10-slim
WORKDIR /usr/src/java-code
COPY . /usr/src/java-code/
RUN mvn package

WORKDIR /usr/src/java-app
RUN cp /usr/src/java-code/target/*.jar ./app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]

These are the most important remarks:

  • Instead of using a JRE as the base image, we're using one with Maven on it. That one is a layer on top of the Java image, so both tools will be available.
  • The first part of the Dockerfile is copying the java code to a folder in a container and running mvn package. Remember that this step will be executed *while building the image* and, as a result, the jar file will be committed to that image layer inside a new target folder, as expected when we run that maven command.
  • The second part of the Dockerfile is similar to the one presented above. The only difference is that now we're not taking the jar file from the host's context, but using it from the previous image step.
  • Since we're now copying all the contents of the directory to the docker image, it makes sense to create a `.dockerignore` file to avoid copying things we don't need there (see the repository for an example).

Combine it from docker-compose

We reuse our existing docker-compose.yml file to build the image with this new approach, changing the line pointing to the Dockerfile to be

dockerfile: Dockerfile-build

Then we execute:

$ docker-compose build

We’ll see how the build process is packaging the java application, downloading all required dependencies and running the tests. The subsequent steps to build the image are taking the resulting .jar file and copying to a different folder.

As you might expect, we can now run the container with the same result as before, using docker-compose up.

What are the differences?

Let’s use the same structure as before (Recap sections) to show you what happened in this case:

  • You used docker to build the application (so you don’t need to have Maven nor Java installed on your machine).
  • You used docker-compose to build and run the application.
    • You built and run the container in one command, using docker-compose up

That could be seen as an advantage but remember my previous comments: that scenario has just a few real-life applications.

Another noticeable difference is the docker image size. If we run docker images with the image built from the first dockerfile, I get a result around 300 MB:

REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
holaweb                  latest              ada28f26d28a        12 minutes ago      302MB

However, if we do that after building the image with Dockerfile-build, the image size gets quite bigger:

REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
holaweb                  latest              e3330a5ad9cb        19 seconds ago      482MB

It’s the expected result since now our image has more layers and also includes the source code in it.

Accessing the source code

The last difference I want to point out is that you can access the source code when running a container. To do that, you can attach a bash terminal to a running container, for instance, when running multiple instances, we can do this:

moises$ docker-compose exec --index=1 hola /bin/bash
root@77fcfddd5402:/usr/src/java-app# ls -l /usr/src
total 8
drwxr-xr-x 1 root root 4096 Jul  8 17:04 java-app
drwxr-xr-x 1 root root 4096 Jul  8 17:04 java-code
root@77fcfddd5402:/usr/src/java-app# ls /usr/src/java-code/
Dockerfile    README.md       pom.xml  target
Dockerfile-build  docker-compose.yml  src
root@77fcfddd5402:/usr/src/java-app#

How can CI tools help here?

Even though this is a working experiment, there are only a few real-life scenarios in which building your Java application with Docker by yourself might be useful. If you don’t need a complex setup on your machine to build the application, you can rely on tools like Jenkins Pipelines, CircleCI, GitLab, etc.

These tools already build your apps in Docker containers out-of-the-box, so you just need to configure them properly and use their pipeline definition language to specify how to build your code - in this case, using maven.

Summary

In this post, we went through different steps to see how we can containerize a Java Spring Boot application using Docker.

  • First, we created a simple Dockerfile and we built the image and run it inside a container using plain docker commands.
  • After that, we introduced docker-compose and saw how it can help to simplify the setup and execution. This would be even more visible when multiple services coexist. We also covered how to easily scale up our service.
  • Last, we had a look at the possibility of building the application using Docker too and reviewed the reasoning for doing that.

I hope you found the guide useful. Use a comment or the Facebook page to give me some feedback!

Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Málaga, Spain https://thepracticaldeveloper.com

Comments