How To Deploy a Spring Boot Web App with Docker
A real-world scenario for deploying and maintaining a Spring Boot web application and a MySQL database
Introduction
In this tutorial, I’ll walk you through the process of deploying a web app with Docker. We will deploy a simple guestbook web app written in Java using the Spring Framework (code available on GitLab). The web app uses a MySQL database in the background to persist guestbook entries. The deployment diagram for the whole application stack looks as follows:
We’ll create three containers:
- The Webserver runs our web app and serves HTML content. We will configure the Docker Host to route all incoming traffic on port 80 to this container (see Published Ports). It will be the only container that is reachable from outside the Docker Host.
- The MySQL Server stores our guestbook entries. A docker volume will be attached to this container to persist the data permanently on the Docker Host, rather than just for the lifetime of the container.
- The Inspector works as a proxy container. It will allow us to connect to the MySQL container and run SQL commands in order to inspect or maintain the database. Furthermore, we’ll import the initial database schema via the inspection container.
Prerequisites
Here’s what you’ll need up front to begin with this tutorial.
- A Docker Host: A system with Docker installed (installation guide for Ubuntu, Windows, MacOS and more). This system is going to be the place where you deploy the web app. In my case it is a DigitalOcean Droplet.
- A web application: Use your own or go ahead with the simple guestbook app. When using your own application, I assume you’re familiar with the process of deploying it manually without docker. You’ll have to implement this process within the docker environment, which might vary a bit from what is being presented in this article when you’re not using a java spring application.
- A
schema.sql
file which contains the SQL code for creating your database schema. For the guestbook application, this file can be found in the project root.
Step 1: Build the Docker Image
We start by creating a Docker Image for the web application. After that, we can create running instances of this Image, which are called Docker Containers. Many containers of the same image can exist at the same time. Scaling an application with docker is one practical application of having multiple containers running the same image; You increase the number of running containers using the same image and balance traffic between them with a Load Balancer. A Docker Image is built using a Dockerfile. A Dockerfile is nothing more than a step-by-step guide on how to take your application into operation. Rather than using natural language, you’ll use a very structured and machine readable syntax for documentation (see Dockerfile reference). This leaves you with a crystal clear documentation on how to run your app and at the same time enables to reproduce your system on any environment that has Docker installed.
The Dockerfile for the guestbook app looks as follows:
FROM openjdk:8-jdk-alpine
COPY build/libs/guestbook*.jar app.jar
ENTRYPOINT ["java","-Dspring.profiles.active=prod","-jar","/app.jar"]
Let’s break it into pieces.
FROM: Specifies the Docker Image we inherit from. There are plenty of Docker Images available on Dockerhub. Dockerhub is the official Docker Registry and is added by default to your local registries. You can add more registries with the docker login command. For example docker login registry.gitlab.com
adds the GitLab registry. For now we are fine with Dockerhub. We use the openjdk image. This means we’ll start in an environment in which java is already installed — one thing less to care about.
COPY: When running gradle bootRun
from the project root, gradle will generate a runnable .jar file into build/libs
. This file will be copied into the container’s root directory and renamed to app.jar
. Thanks to Spring Boot’s Embedded Webserver, we don’t need more than this .jar
file and a java runtime to run the guestbook application.
ENTRYPOINT: This is the command that is going to be executed when a container of this image is created. The ENTRYPOINT
instruction can be enhanced with the CMD
instruction which in turn can be provided at the time of container creation. For instance, consider the following docker run command which would start a container of the <image_name>
docker image:
$ docker run <image_name> echo 'Hello world'
The echo ‘hello world’
at the end will be appended to the ENTRYPOINT
command, such that in our case the full startup command would look like (though not really meaningful):
java -Dspring.profiles.active=prod -jar /app.jar echo 'Hello world'
Now back to the actual ENTRYPOINT
command from the Dockerfile
. The spring.profiles.active=prod
option tells spring to use the application-prod.properties file under src/main/resources
which contains the database information for production.
Next we create a runnable .jar
file which will contain the guestbook application.
$ gradle bootJar
The .jar file will be generated to build/libs
.
Following this, build the docker image with as follows:
$ docker build -t registry.gitlab.com/mikenoethiger/guestbook:latest .
The .
(dot) at the end of the command specifies the location of the Dockerfile
which in this case is the current directory. With -t
you specify an image name that consists of the base name, registry.gitlab.com/mikenoethiger/guestbook
, and the tag, latest
. In order to publish the docker image to the GitLab registry eventually, the following naming pattern has to be implemented according to GitLab’s Documentation:
Your image will be named after the following scheme:
<registry URL>/<namespace>/<project>/<image>
If you want to build the image on your local machine only, you can skip the next steps and proceed with Step 2.
Before we can push to the GitLab registry, we need to authenticate properly. This can be achieved with the docker login command. After executing the following command, you’ll be prompted for your GitLab username and password.
$ docker login registry.gitlab.com
Next, push the image to GitLab using docker push.
$ docker push registry.gitlab.com/mikenoethiger/guestbook:latest
Images are pushed using the image name that was specified at the time of docker build -t <image_name> …
. The image should now appear on the registry, which is in my case: https://gitlab.com/mikenoethiger/guestbook/container_registry
Step 2: Setup the Docker Environment
As outlined in the Deployment Diagram, our Application Stack consists of a web app container, a MySQL container, an inspection container as well as a docker network, that connects all the containers together. We are going to setup the MySQL and inspection container as well as the Docker Network. We will then copy an SQL file into the inspection container and execute it using the MySQL client.
Let’s begin with the docker network to which we will connect the other two containers afterwards. The following command creates a new Docker Network called guestbook
.
$ docker network create --driver=bridge guestbook
Verify that the creation worked properly by listing all your networks.
$ docker network ls
Next let’s create a MySQL Container.
$ docker run --name guestbook_db -v guestbook_db:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=guestbook --network guestbook -d mysql:5.7
docker run
is the command that starts new containers. In this case we craft a container from the mysql:5.7
image. We name the container guestbook_db
and map a Docker Volume (also called guestbook_db
) at /var/lib/mysql
, which is the place inside the container where MySQL stores its data. This way we keep the data persistent even if the container is destroyed. Docker Volumes live separately from Docker Containers. When you delete the database container, you can just create a new one and map it to the previously created guestbook_db
volume. This is possible because docker always synchronizes data in var/lib/mysql
of your container with a directory on the host machine. Furthermore, we connect the container to the guestbook network. The -e
options are environment variables that are used by the MySQL image when starting the container which makes the database configuration more dynamic. We specify a root password and a database that will be created upon startup. The -d
option tells docker to run the container in detached mode, which ensures that the shell won’t be blocked by the container process.
Check whether the container has been created properly by listing all running containers.
$ docker container ls
You can also list the volumes, which should display our guestbook_db
volume.
$ docker volume ls
If you’re curious about where docker actually stores the volume data on the host, run the inspect command.
$ docker volume inspect guestbook_db
Next let’s create the inspection container.
$ docker run -dit --name guestbook_inspector --network guestbook alpine ash
The -dit
option ensures that we can attach STDIN, STDOUT and STDERR of our shell to the container (read this chapter for detailed information). We use the alpine image, which is a naked, lightweight linux with nothing special installed yet. The last parameter, ash
, defines the entrypoint of the container, which ensures that the alpine shell (which is called ash) will be started upon container creation.
Next, let’s copy the SQL file to the inspection container (assuming the fileschema.sql
is in your current working directory).
$ docker cp schema.sql guestbook_inspector:/tmp/schema.sql
cp
is the docker command, schema.sql
the path to the file on the host, guestbook_inspector
the container name to which the file should be copied to and/tmp/schema.sql
is the target directory inside the container.
Now let’s attach our STDIN, STDOUT and STDERR to the container, in order to set it up properly.
$ docker container attach guestbook_inspector
Since we started the ash shell, we can run common linux commands, such as ls
, pwd
or cd
. Since we connected the inspection container to the guestbook network too, we should be able to ping our MySQL container. Let’s test it.
(From now on I will prepend the command snippets with a hash #, which indicates that we are attached to a container.)
# ping guestbook_db
If everything went well so far, you should receive responses. You can reach all containers connected to a network by their container name, which automatically works as their hostname. You could also find out the ip address of a container by running docker container inspect container_name
and ping the IP address.
Next we want to install a MySQL client and an HTTP client which will allow us to debug and maintain the application stack.
# apk update && apk upgrade && apk add mysql-client curl
apk is alpine’s package manager. Here we use the &&
operator to execute several commands consecutively. The consecutive commands are only executed when the previous command succeeded. The first two commands update the package index and upgrade all installed packages. The last one installs the mysql-client
and curl
programs.
Verify the installation by searching for the programs.
# which mysql
# which curl
We now have a MySQL client as well as an SQL file in /tmp/schema.sql
so let’s run it (you’ll be prompted for the password, which is secret
if configured everything according to this article).
# mysql -h guestbook_db -u root -p guestbook < /tmp/schema.sql
-h guestbook_db
is the MySQL server address, remember we can use the container name here. -u
is the username and -p
ensures we will be prompted for the password. guestbook
is the target database. <
redirects STDIN to /tmp/schema.sql
, ensuring the content of the file will be executed as an SQL query on the MySQL server.
Your schema should now be created. We can verify that, by connecting to the MySQL server and list all tables.
# mysql -h guestbook_db -u root -p guestbook
# Enter password:
# MySQL [guestbook]> show tables;
From now on, the inspection container is your tool to maintain the database. You can use the MySQL container to connect to the database and curl to make HTTP requests to your webapp. Move over to the official MySQL client documentation or curl documentation respectively for more information.
Next we want to disconnect from the container. First terminate the MySQL connection:
# MySQL [guestbook]> exit
Following this, hit CTRL+P
CTRL+Q
successively in order to detach STDIN, STDOUT and STDERR from the container which will bring you back to your local shell session.
Step 3: Deploy the Webapp
This chapter addresses the deployment of your webapp.
I will describe the whole deployment procedure in a way that is repeatable such that you can repeat the given steps to deploy a newer version of your web app.
Let’s stop (docker stop) and remove (docker rm) the running webapp container.
$ docker container stop guestbook && docker container rm guestbook || true
The || true
at the end suppresses error signals from the previous commands, thus the command will always succeed, even if the container didn’t exist (this is especially useful when deploying in a CI/CD environment).
Next, we login to the GitLab registry, which is obsolete if you already logged in or you don’t work with a remote registry.
$ docker login registry.gitlab.com
Now let’s pull the latest image.
$ docker pull registry.gitlab.com/mikenoethiger/guestbook:latest
And finally run the webapp container.
$ docker run -d -p 80:8080 --name guestbook --network guestbook registry.gitlab.com/mikenoethiger/guestbook:latest
Most of the options were already discussed in Step 2. -p
binds port 8080 of the container to port 80 of the host machine. Since we’re connected to the guestbook network, the web app can reach the database with its container name, which is guestbook_db
.
Following this, open http://127.0.0.1 in a browser to verify the guestbook web app is showing up.
If you have problems accessing the web app, my first troubleshooting step would be to check whether the web app can be reached with a ping
from the inspection_container
. I would use curl
, the HTTP client that was installed in Step 2, to make an HTTP request which should return some HTML if the connection succeeds.
$ docker container attach inspection_container
# ping guestbook
PING guestbook (172.20.0.3): 56 data bytes
64 bytes from 172.20.0.3: seq=0 ttl=64 time=0.696 ms
64 bytes from 172.20.0.3: seq=1 ttl=64 time=0.187 ms
# curl guestbook
<!DOCTYPE HTML>
<html>
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Enter Your Name</h1>
<form action="/register" method="post">
<input type="text" name="name" placeholder="John Doe" required>
<button type="submit">Submit</button>
</form><tr>
<p >Mike was here!</p>
</tr><tr>
<p >John Doe was here!</p>
</tr>
</body>
</html>
When these debugging steps all worked but the web app still can’t be accessed, the problem is likely to be on your host machine. Perhaps the port is protected by a firewall?
Step 4: Monitor the Application Stack
Finally, I want to share some commands and procedures, that have helped me to monitor, troubleshoot and maintain a running docker application stack.
List Containers
One of dockers most important command is the the docker container ls
command. It will display essential container information, such as ID, up-time, names and published ports. Append with -a
to display all containers, including stopped ones.
$ docker container ls -a
The ls
command is available for almost every docker object, such as images, volumes and networks.
Show Container Logs
With docker logs
you can display the logs of a container. Append --follow
to get live updates of latest STDOUT and STDERR messages.
$ docker logs --follow
Show Detailed Docker Information
With docker inspect
you can show more detailed information about docker objects, such as internal IP address, physical volume path etc.
$ docker container inspect guestbook
$ docker volume inspect guestbook_db
Debug the Web App Container
Sometimes the web app container does not start properly and it’s the easiest if you could actually go into the container and run unix commands to debug the problem properly instead of having to rebuild the Image every single time and read container logs. This can be achieved by starting the container slightly different.
$ docker container run -dit --name guestbook_debug -p 80:8080 --network guestbook cr.gitlab.fhnw.ch/mike.noethiger/ip12-18vt_webshop_2/master:latest ash
I have highlighted the essential parts, which is the -dit
that simulates a terminal and the ash
which overrides the default ENTRYPOINT, ensuring the ash shell is started upon initialization.
Now you can attach your standard streams to the container the same way we did in Step 2 with the inspection_container.
$ docker container attach guestbook_debug
You’re ready to debug, fix the problem, apply the fix to the Dockerfile and rebuild the image.
Remove Stopped Containers
One common task is to remove stopped containers because you can’t start a new one with the same name, as long as the old one is still available (stopped containers also count). Instead of having to docker container rm
every single container, use:
$ docker container prune
Which will remove all stopped containers at once. Make sure you don’t have any other stopped containers that shouldn’t be deleted!
Conclusion
In this tutorial we have built and deployed a common web application with docker, which includes:
- Creating a Dockerfile and building a Docker Image from it.
- Publishing the Image to the GitLab registry.
- Creating a Docker Network, that connects all involved containers.
- Creating a MySQL container to store web app data.
- Importing an SQL file to the MySQL container.
- Using an inspection container to make HTTP requests and connect to the MySQL server for debugging and maintenance purposes.
- Deploy the web app using Docker.
Eventually, we have seen common procedures and commands which help maintaining the application stack. The guestbook project resources are available on GitLab.
Thank you for reading 🤓 I wish you success with using docker in your personal project!