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.

This is only true if your host OS is a Linux machine. If you’re like me, running Docker on Mac OS X, you’ll have to create a Linux environment to run Docker containers on (using docker-machine).

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 mysql alias is actually defined as well, which means you’re also able to ping it:

docker-linked-mysql

Finally, several environment variables, coming from the MySQL container, have been added as well. They all get the alias (mysql) as prefix, so you can use the following command to check them all:

printenv | grep MYSQL
Note that the alias is converted to uppercase when looking for the environment variables

docker-mysql-env

Most of these environment variables look familiar. The environment variables we had to configure earlier are available next to the IP address, MySQL version, and various other information. This is useful, because this means we can use these environment variables to connect to the database within our application.

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://${MYSQL_PORT_3306_TCP_ADDR}:${MYSQL_PORT_3306_TCP_PORT}/${MYSQL_ENV_MYSQL_DATABASE}
    username: ${MYSQL_ENV_MYSQL_USER}
    password: ${MYSQL_ENV_MYSQL_PASSWORD}

What we did here is that we used the environment variables to create the JDBC connection string and to use the other environment variables as our username/password. Spring boot allows us to easily use environment variables within application.yml or application.properties.

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: local-user
    password: localpassword
    platform: mysql
    initialize: false
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect

---
spring:
  profiles: container
  datasource:
    url: jdbc:mysql://${MYSQL_PORT_3306_TCP_ADDR}:${MYSQL_PORT_3306_TCP_PORT}/${MYSQL_ENV_MYSQL_DATABASE}
    username: ${MYSQL_ENV_MYSQL_USER}
    password: ${MYSQL_ENV_MYSQL_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 \
    g00glen00b/spring-boot-jpa-docker-webapp

To test it out you can go to http://localhost:8080/superhero. As I explained earlier, if you’re on Linux, you can run Docker containers native. If you’re not on Linux, your Docker machine probably runs inside a virtualbox.

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

Now, in my case it returns 192.168.99.100, which means I can go to http://192.168.99.100:8080/superhero to view the application. As you can see, the application should work properly, on Docker containers!

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

docker-commands

Introduction to Docker compose

We’ve now seen two components of the Docker platform so far. Docker for managing containers, and Docker machine for managing the Docker machines/hosts the containers can run on.

If we wrap up everything we’ve seen so far in order to run the application on Docker we need to execute the docker run command twice to run the MySQL container and to run the application container. But what are you going to do with those commands, put them in a shell script?

The next component of the Docker platform is 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 inside the src/main/docker folder:

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
  links:
    - demo-mysql:mysql
  ports:
    - 8080:8080

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

To execute this command, 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, you have to make sure your working directory is src/main/docker and then you run:

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.

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

IT Consultant with a passion for JavaScript. Experienced in the Spring Framework and various JavaScript frameworks.

  • 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