Using Docker containers for your Spring boot applications

In my latest tutorials I built a small web application using a database to retrieve some data and show it on a simple webpage. In this tutorial I will be using the same codebase, but this time I will be using Docker containers to run the application.

Docker?

You probably already heard about Docker, but in case you didn’t, Docker is an open platform to build, ship and run applications by wrapping them in “containers”.

If you write an application like I did in my last tutorial, and you want to share it with the rest of the world, what do you do if your application relies on a database or other components? What do you do if someone wants to run your application, but doesn’t even have Java?

spring-boot-docker

There are several solutions to this. You might say, create a virtual machine with everything on it, and make the user run that virtual machine. This will probably work, but sharing virtual machines isn’t probably a good idea, besides that they tend to take a lot of resources, because you’re actually running an entire environment (OS + libraries + …) virtualized.

© Docker

© Docker

Docker on the other hand makes your applications run on a Docker engine, which communicates with the host OS. They share the operating system (= host OS), and if possible they also share the libraries/resources. In stead of sharing Docker containers, you share a configuration file which contains instruction about how the container should be setup.

Originally, this was only true for Linux machines and other operating systems had to use a virtual machine (Virtualbox). However last year they changed it so you can run containers natively on Windows and macOS as well.

Docker also has a public repository with all kinds of Docker images, called the Docker hub. This means that if you have a container that requires Java 8, you can simply write your own Docker image configuration file (Dockerfile), telling to use the java image.

Each Docker image is like a Matryoshka doll, for example, if we take the Dockerfile for the java:8 image, it uses the buildpack-deps:jessie-scm image, which in turn uses buildpack-deps:jessie-curl, which uses debian:jessie.

matryoshka

Using Docker containers

In this example I will be creating two Docker containers:

  • A container for the MySQL database
  • A container for the Spring boot application

Let’s first start by setting up the container for the MySQL database. This is quite simple, because MySQL already provided a public image for us to use on the Docker hub.

All we have to do is run a container, it will automatically download the mysql image if it doesn’t already have it installed.

The command to run a MySQL container is:

docker run -d \
    --name demo-mysql \
    -e MYSQL_ROOT_PASSWORD=p4SSW0rd \
    -e MYSQL_DATABASE=demo \
    -e MYSQL_USER=dbuser \
    -e MYSQL_PASSWORD=dbp4ss \
    mysql:latest

docker-pull-mysql

Let’s break the command down a bit and look at each parameter individually. First of all we have the -d flag. We use this flag to run the container in detached mode, which means that it will run in a separate background process. If you’re testing the container out, leaving the -d flag or adding the -it flag can be useful. Leaving the -d flag away causes all output to be printed to STDOUT, and the process will stay open on the terminal.

Using the -it flag on the other hand allows you to get an interactive terminal prompt, so you can execute commands on the container by yourself.

If we look further to the --name parameter, this is the easiest one, since it tells us what the name of the container will be.

Further down we have several -e parameter, which stands for environment variables. We configured several environment variables to set up the database. The first one being the root password to connect to MySQL. The other three parameters are used when you want to setup a database as well. In this case I’m setting up a database called demo, and I have a user that is granted access to that demo, called “dbuser” and the password being “dbp4ss”. Feel free to change any of them, later in this tutorial you’ll see that it doesn’t really matter.

At the end of the command we enter the image we’re using, which is the latest MySQL image.

Setting up your own Docker image

For our Spring Boot application we’ll have to set up our own Docker image. To do that you create a Dockerfile. I placed mine inside the src/main/docker folder, which is the folder Spring uses in their demo’s as well.

Let’s start with a very simple Dockerfile first:

FROM java:8

Simple, isn’t it? All we said is that, for this image to run, we need the java:8 image. If you remember our matryoushka doll a bit earlier, we’re now putting the java:8 doll inside our own custom made doll.

Now, the next part is to add a Maven plugin that creates the Docker image:

<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.4.11</version>
    <configuration>
        <imageName>g00glen00b/${project.artifactId}</imageName>
        <dockerDirectory>src/main/docker</dockerDirectory>
        <resources>
            <resource>
                <targetPath>/</targetPath>
                <directory>${project.build.directory}</directory>
                <include>${project.build.finalName}.jar</include>
            </resource>
        </resources>
    </configuration>
</plugin>

So, what do we have here, first we tell the plugin what our image name will be. I chose for g00glen00b/${project.artifactId}, but you can choose your own name. However, if you want to push your image to the Docker hub by yourself, you’ll have to change the g00glen00b/ part as well to your own username.

Now, we also defined the directory containing the Dockerfile and lastly we also provided the directory containing our JAR and the JAR filename itself. The reason for this is that, later in this tutorial, we will be using that JAR in our Docker container.

I also changed my artifact ID to spring-boot-jpa-docker-webapp and I changed the build name to the artifact ID:

<build>
    <finalName>${project.artifactId}</finalName>
</build>

Now, if you execute the following command, the Docker image will be created:

mvn clean package docker:build

The docker:build step is all you need, but it requires your project to be built, so the best thing to do is to package it as well.

maven-docker-plugin

As you can see, the Docker image is being built by the Maven plugin, making it ready for us to use.

Linking containers

Now, because our application Docker container requires a MySQL container, we will link both containers to each other. To do that we use the --link flag. The syntax of this command is to add the container that should be linked and an alias, for example --link demo-mysql:mysql, in this case demo-mysql is the linked container and mysql is the alias.

We’re also going to make the 8080 port open, available to use on the host machine. The entire command will be:

docker run -it \
    --name spring-boot-jpa-docker-webapp \
    --link demo-mysql:mysql \
    -p 8080:8080 \
    g00glen00b/spring-boot-jpa-docker-webapp

You can see that I also used the -it flag, which allows us to run the Docker container interactively as a root.

Now, there are a few things you can check, to see how linked containers behave. First of all, enter the following command:

cat /etc/hosts

Linux users will probably recognize this command already, but this is the hosts file containing host mapping (to IP addresses). If you look at the entries in this file, you’ll see that the demo-mysql alias is actually defined as well, which means you’re also able to ping it. For me the entry looks like this:

172.17.0.2      mysql f269609025f1 demo-mysql
Using these aliases is the way to go to link containers. If you want to connect to the other container, you just specify `demo-mysql` as the hostname. Previously environment variables were also a possibility, but they are now deprecated.

Running the application

Now, let’s go back to our application. First of all, let’s change the application.yml file. We’re going to define a new profile called container:

---
spring:
  profiles: container
  datasource:
    url: jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}

As you can see, we’re using quite some environment variable placeholders here. By using environment variables, we can try to couple the application container as loosely as possible to the database container.

Spring already provides several cloud connectors, perhaps they might work as well to provide this out of the box. I tried to be authentic to the original example in this tutorial.

Anyways, while the MySQL container already provides us with a database, it doesn’t provide us with the tables, nor the data we’d like. To fix that, you can create a schema.sql and a data.sql file and put it onto the classpath. Spring boot will automatically scan that and execute it on startup.

You can also add platform-specific files like schema-mysql.sql and data-mysql.sql. In this example I will be using those. First of all create schema-mysql.sql and add the following content:

CREATE TABLE IF NOT EXISTS `superhero` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(32) NOT NULL,
  `first_name` VARCHAR(32),
  `last_name` VARCHAR(32),
  `good` bit(1),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1;

Then create the data-mysql.sql file and add the following content:

DELETE FROM `superhero`;
INSERT INTO `superhero` (`name`, `first_name`, `last_name`, `good`) VALUES
  ('Superman', 'Clark', 'Kent', 1),
  ('Silver Banshee', 'Siobhan', 'McDougal', 0);

Put those files inside the src/main/resources folder so they’re available on the classpath. However, to make Spring boot use those files, you have to set the spring.datasource.platform property. So change that inside the application.yml file.

Also, the initialization is enabled by default, which means it will also be executed if you run the application locally, which might not be good (or maybe it is). Anyways, since we created a separate profile, all we have to do is to set the spring.datasource.initialize property to false by default, and to set it back to true for the container profile.

Eventually your application.yml file will look like this:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo
    username: root
    password: p4SSW0rd
    platform: mysql
    initialize: false
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect

---
spring:
  profiles: container
  datasource:
    url: jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}
    initialize: true

Be aware that the properties above the container-profile are used to run the application with a local database, it does not have anything to do with this tutorial. In fact, when we use the container profile, you can clearly see that we’re overriding most of those properties.

Now the final step is to change the Dockerfile a bit more. The new Dockerfile should look like this:

FROM java:8
ADD spring-boot-jpa-docker-webapp.jar app.jar
RUN bash -c 'touch /app.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dspring.profiles.active=container","-jar","/app.jar"]

First of all we map the spring-boot-jpa-docker-webapp.jar which was included by using the Maven plugin to a JAR named app.jar.
Then we provide the entrypoint of the container. This command will be executed if the container is ran. IN this case we execute our JAR using the container profile, which we provided using the -Dspring.profiles.active=container parameter.

Now all you have to do is to execute the following command again to rebuild the Docker image:

mvn clean package docker:build

Testing it out

To test it our we’re first going to remove our existing spring-boot-jpa-docker-webapp container. In case you’re still inside the interactive terminal as a root on the container, you can use the exit command to return back to your host machine.

After that execute the following command:

docker rm spring-boot-jpa-docker-webapp

And then run the new container using the following command:

docker run -d \
    --name spring-boot-jpa-docker-webapp \
    --link demo-mysql:mysql \
    -p 8080:8080 \
    -e DATABASE_HOST=demo-mysql \
    -e DATABASE_PORT=3306 \
    -e DATABASE_NAME=demo \
    -e DATABASE_USER=dbuser \
    -e DATABASE_PASSWORD=dbp4ss \
    g00glen00b/spring-boot-jpa-docker-webapp

As you can see, I’m defining all the environment variables I need to properly run the application container. Be aware that the DATABASE_HOST environment variable uses the demo-mysql host which was defined in /etc/hosts. We don’t have to refer to an IP address here. To test it out you can go to http://localhost:8080/superhero.

If you are running the container on a Docker machine, you won’t be able to use localhost, but you should use the IP address of the Docker machine. To retrieve the IP of the Docker machine you can use the following command (if you installed the Docker machine with the name default):

docker-machine ip default

There are a few other commands that might be useful. For example, if you didn’t know that your docker-machine was called “default”, you could use the following command to retrieve a list of all docker machines, including the IP and the type:

docker-machine ls

And finally, to see which Docker containers are running, you can use the following command:

docker ps

Introduction to Docker compose

So far so good, we’re able to run our entire application by starting two containers by using the docker run command twice. But what are you going to do with those commands, put them in a shell script?

Docker has a tool to simplify this as well called Docker compose. With Docker compose you can compose a single configuration file that contains all configuration of all containers that have to be ran in order to execute your application.

So, let’s create a docker-compose.yml file in the project base folder:

version: '2.1'

services: 
  demo-mysql:
    image: mysql:latest
    environment:
      - MYSQL_ROOT_PASSWORD=p4SSW0rd
      - MYSQL_DATABASE=demo
      - MYSQL_USER=dbuser
      - MYSQL_PASSWORD=dbp4ss
  spring-boot-jpa-docker-webapp:
    image: g00glen00b/spring-boot-jpa-docker-webapp
    depends_on:
      - demo-mysql
    ports:
      - 8080:8080
    environment:
      - DATABASE_HOST=demo-mysql
      - DATABASE_USER=dbuser
      - DATABASE_PASSWORD=dbp4ss
      - DATABASE_NAME=demo
      - DATABASE_PORT=3306

The configuration is quite simple, and looks very similar to the commands we executed.

Health check database

Before we start running the containers using Docker compose, we have to fix a few things. Right now, when we run the application container, we expect the database container to be running properly already. This can be an issue, because if we us Docker compose, we’ll run both containers at the same time, so there’s no guarantee that the database will be up and running when we run our application.

Docker compose had a feature called healthcheck to wait for a container before it is executed, but this feature is no longer there if you use the Docker compose v3 file format. The proper way to implement this now is to implement it by yourself, so let’s do that now.

First of all, create a file called wrapper.sh in src/main/docker with the following content:

#!/bin/bash

while ! exec 6<>/dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}; do
    echo "Trying to connect to MySQL at ${DATABASE_PORT}..."
    sleep 10
done

java -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=container -jar /app.jar

As you can see, we moved the Java command to this script, and used a while loop first to see if the database container is running already on the given port (probably 3306). If it isn’t running, it will wait 10 seconds before trying to connect again, until it is finally running.

Now, we also have to change the Dockerfile a bit, because we’ll have to execute a shell script rather than a Java application:

FROM java:8
ADD spring-boot-jpa-docker-webapp.jar app.jar
ADD wrapper.sh wrapper.sh
RUN bash -c 'chmod +x /wrapper.sh'
RUN bash -c 'touch /app.jar'
ENTRYPOINT ["/bin/bash", "/wrapper.sh"]

Also notice that we’re using chmod first to make the script executable. Finally, we also have to change the Maven plugin to include the wrapper.sh file. Normally this is no issue because the folder containing the Dockerfile is automatically included to the container image, but you never know:

<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.4.11</version>
    <configuration>
        <imageName>g00glen00b/${project.artifactId}</imageName>
        <dockerDirectory>${docker.baseDir}</dockerDirectory>
        <resources>
            <resource>
                <targetPath>/</targetPath>
                <directory>${project.build.directory}</directory>
                <include>${project.build.finalName}.jar</include>
            </resource>
            <resource>
                <targetPath>/</targetPath>
                <directory>${docker.baseDir}</directory>
                <include>wrapper.sh</include>
            </resource>
        </resources>
    </configuration>
</plugin>

I added a new <resource> and created a property called ${docker.baseDir} which I defined like this:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <docker.baseDir>src/main/docker</docker.baseDir>
</properties>

Now we can rebuild the container using the docker:build Maven goal.

Running Docker compose

To execute Docker compose, we’re first going to remove our existing containers so that we’re certain that Docker compose created them.

To do that, you first have to stop your containers:

docker stop demo-mysql
docker stop spring-boot-jpa-docker-webapp

And finally you can remove them using the following command:

docker rm demo-mysql
docker rm spring-bot-jpa-docker-webapp

Now, to run the Docker containers using Docker compose, use the following command:

docker-compose up

Running this command will show you the stdout of both containers.

docker-compose

Similar to Docker, you can provide the -d flag to execute them using background processes. If you go back to your application, you’ll see that it works fine again, which means we’re now ready to use Docker containers for our Spring boot applications!

Achievement: Dockerize the world

If you’re seeing this, then it means you successfully managed to make it through this article. If you’re interested in the full code example, you can find it on GitHub. All the Docker-related files can be found inside the src/main/docker directory.

Tagged , , , .

g00glen00b

Consultant at Cronos and Tech lead at Aquafin. Usually you can find me trying out new libraries and technologies. Loves both Java and JavaScript.

  • soundaraws mani

    Thanks for very clear description.

  • Probal Sikder

    Helped me a lot. Thanks.

  • Fernando Fradegrada

    Great post! Thanks!

  • Ganesh Pagade

    Now that Docker Compose has deprecated the linking of environment variables (https://docs.docker.com/compose/link-env-deprecated/), could you write about clean way of making MySQL details available to the application?

    • Thanks for notifying me about the change, I’ll see if I can fix this this weekend!

    • Hey, I just want to tell you that I updated the article.

      • Ganesh Pagade

        This is what I ended up doing. You too duplicated the MySQL details into the webapp environment. Not so clean than linking the env. vars. I guess.

        • Well I guess this isn’t too big of a problem since they’re all configured in the same file. In the container itself I think it has the advantage that we’re no longer tightly coupled to the MySQL container. We could, for example, now use the environment variables and refer to a completely separate MySQL database.

  • Hafizullah Nikben

    Great article!! enjoyed reading it and now fully understand key concepts! thank you

  • Iqbal Yusuf

    Where are you injecting the environment variable for MySQL host and port. MYSQL_PORT_3306_TCP_ADDR

    • This should be available if you link a MySQL container to the application container.

  • OohWee

    The build fails with this error:

    Caused by: org.apache.http.conn.HttpHostConnectException: Connect to localhost:2375 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused

    • I tried it myself, and indeed, there’s an issue with the Docker maven plugin. I updated it to their most recent version and it works though. I also committed the version change: https://github.com/g00glen00b/spring-samples/commit/6b884129f3986bbb94b019e98b19de5367235c1e

      • Vijay Bhaskar

        Hi,
        Nice post on spring-boot with docker integration. Thanks alot. It is very helpful for me doing POC on docker

        The actual issue is not with the version of Docker maven plugin.
        I added dockerHost property as below to pom.xml

        vijaydoc/${project.artifactId}
        src/main/docker
        https://192.168.99.100:2376
        C:Usersvpedapudi.dockermachinemachinesdefault

        It works for me

        Thanks
        Vijay