Docker is a popular mechanism for packaging and distributing applications. The Docker system allows users to package applications into images for distribution, and then turn those image files into containers that run on a user's computer or on a server. Docker also provides tools that make it easy for multiple applications to run at the same time in separate containers that communicate with each other and cooperate to acheive some goal.
To help make all of this more concrete, in these lecture notes I am going to be showing an example in which I package a Spring Boot application into an image and package the MySQL database into a second image. I will then demonstrate how to get those two to work together to make a complete system.
What is a Docker image? An image is a packaging mechanism that packages an application together with the dependencies it needs to operate. For example, to run a Spring Boot application we will typically compile the Spring Boot app into a jar file and then use a Java runtime environment to run that jar file. Docker will allow us to package both the jar file and the Java runtime environment together into an image that we can run. This is aimed at making it easier to distribute applications: instead of giving someone a jar file and then telling them they need to set up a Java runtime to run it, we can give them a Docker image that has everything needed to run the application. To run those images users will need to have Docker installed on their system, but this is a fairly common thing for servers to have pre-installed.
If you would like to follow along with some of the examples I will be showing in this lecture you will also need to install 'Docker on your computer. The easiest way to do this on a laptop is to install the Docker Desktop application.
For the demonstration in class I used docker installed on a Linux machine.
The simplest way to construct an image is to construct a Dockerfile. This is a text file that includes all of the information that Docker will need to set up the image.
Here is an example of what typically goes into a Dockerfile. The Dockerfile shown here is what I used to construct an image that packages the jar file for my auction application together with a JDK:
FROM eclipse-temurin:21 RUN mkdir /opt/app COPY auction.jar /opt/app EXPOSE 8085 CMD ["java", "-jar", "/opt/app/auction.jar"]
This text goes into a file whose name is literally 'Dockerfile'. Also, I am assuming that I have a copy of the jar file stored in the same directory as the Dockerfile.
There is a lot going on in these few lines of code:
eclipse-temurin:21. That base image comes pre-equiped with a Java 21 JDK optimized for use in a Docker environment.Once we have constructed our Dockerfile we can ask Docker to build an image from it. For example, we can run the commands
sudo docker build -t auction . sudo docker run -dp 8085:8085 auction
in a terminal to create the image from the Dockerfile and then run the image. The run command here runs the container in detached mode, which essentially runs the application in the background. The running container will appear in the Docker desktop application under Containers. There you will see controls that you can use to stop the container.
Running our auction application by itself will only really work if we have a database already installed on the same machine for the server to talk to.
When we distribute our application we will want to distribute it with everything that it needs to run successfully. If we are planning to install our server in a location where the MySQL server and database are not available we will need to make arrangements to distribute our image along with a second image that has MySQL and our auction database installed. Further, we will need to arrange for these two containers to be able to communicate with each other.
Fortunately, Docker offers a relatively simple tool called Docker Compose that makes it easy to run multiple cooperating containers at the same time.
The key to using Docker Compose is to set up a second configuration file, named docker-compose.yaml, in the same directory as our Dockerfile and jar file.
Here is what the initial version of my docker-compose.yaml file looks like:
services:
auction:
build: .
image: auction:latest
restart: always
ports:
- 80:8085
networks:
- auction
environment:
- spring.datasource.url=jdbc:mysql://mysqldb:3306/auction
depends_on:
- mysqldb
mysqldb:
image: "mysql:8.0"
restart: always
ports:
- 3305:3306
networks:
- auction
environment:
MYSQL_DATABASE: auction
MYSQL_USER: student
MYSQL_PASSWORD: Cmsc250!
MYSQL_ROOT_PASSWORD: Cmsc455!
volumes:
- ${PWD}/auction.sql:/docker-entrypoint-initdb.d/auction.sql
networks:
auction:
The first thing to notice here is that a typical YAML file is used to set up a hierarchical collection of configuration commands. At the top level of the hierarchy here we see two entries, services and networks. The networks section contains a single virtual network named 'auction' that we will be using to enable our two containers to talk to each other.
The most important part of the document is the services section, which defines two services, auction and mysqldb. The auction service is essentially the container that will host our Spring Boot app, while the mysqldb service will be a container that runs a copy of MySQL with our auction database installed in it.
The auction service contains a build command which tells docker compose to look for a Dockerfile in the same directory and run that Dockerfile to build the initial image. Separate from that we also have an image setting which will determine the name of the image that we build. The environment option allows us to pass environment values to the auction container when it starts up. In this case, I will be passing an environment value that tells the server to override the spring.datasource.url setting in application.properties with a new value that points to the MySQL server running in the second container.
The mysqldb service is a little different than the auction service. For the database image we are not going to be using a Dockerfile. Instead, we specify a standard Docker base image that comes with MySQL version 8.0 already installed. The environment section of this service specifies that when MySQL starts up it should create a new empty database named 'auction' and also set up a user with the necessary privileges to access that database. To define the structure of the auction database I started by using the MySQL workbench to export a self-contained dump file named 'auction.sql' that defines the tables needed for our database. The volumes option in the service allows us to mount a copy of that file in the database container in the container's docker-entrypoint-initdb.d directory. The MySQL container is set up to look for dump file in that folder and will automatically load any such dump files that it finds there. This will guarantee that our database will be installed in the database container and ready for use.
Both containers include port settings that we can use to connect to the applications from the outside world. The port settings for the application remap the application's 8085 port to the more standard port 80, which is the default port for http. With this setting we can contact our application through the URL
http://localhost
which is a little more user friendly. Likewise, we remap the 3306 port for MySQL to a different port, 3305, so it does not conflict with the MySQL server we already have running on our test machine. Since the MySQL container exposes this 3305 port we can connect to it on that port with the MySQL workbench to monitor the database and run backups.
To build and run our two-container system we simply have to run the commands
sudo docker compose build sudo docker compose up
To shut the containers back down we simply have to execute the command
sudo docker compose down
The final step for us will be to use this system to run our application on a server.
In preparation for doing this we will go to the Docker Hub website to sign up for an account and create a repository. After doing that we can go back to our development machine, log in to our Docker Hub account and then push our auction image to the repository we set up.
Next, we go to a service like AWS and set up a virtual machine. We install the docker and docker compose packages on that virtual machine and then upload our SQL file and a custom docker-compose.yaml file to that machine.
Here is the slightly modified docker-compose.yaml file we will use:
services:
auction:
image: <reponame>/auction:latest
restart: always
ports:
- 80:8085
networks:
- auction
environment:
- spring.datasource.url=jdbc:mysql://mysqldb:3306/auction
depends_on:
mysqldb:
condition: service_healthy
mysqldb:
image: "mysql:8.0"
restart: always
ports:
- 3306:3306
networks:
- auction
environment:
MYSQL_DATABASE: auction
MYSQL_USER: student
MYSQL_PASSWORD: Cmsc250!
MYSQL_ROOT_PASSWORD: Cmsc455!
volumes:
- ./auction.sql:/docker-entrypoint-initdb.d/auction.sql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 1s
retries: 120
networks:
auction:
The auction service no longer contains a build command, since we have already built the image and pushed it to Dockerhub. We do need an image setting that instructs docker compose to pull our pre-built image back down from Dockerhub. (Here you would have to replace <reponame> with the name of the repository you set up on Docker Hub.
I also added a feature that specifies that the auction server container should wait to start up until the database container is fully up and running. This is done by specifying a health check for the database container which will pass only once the database server is fully up and running.