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.
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:22 RUN mkdir /opt/app COPY jpaauction.jar /opt/app EXPOSE 8085 CMD ["java", "-jar", "/opt/app/jpaauction.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:22
. That base image comes pre-equiped with a Java 22 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
docker build -t auction 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: joegregglu/auction:latest restart: always ports: - 8085: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. Later when we push our images to Dockerhub the image name will also specify that we want to push this image to a repository named 'joegregglu' that I have set up ahead of time. 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.
To build and run our two-container system we simply have to run the commands
docker-compose build docker-compose up
To shut the containers back down we simply have to execute the command
docker-compose down
In preparation for setting our images up on a server and running them there I need to do two additional steps. The first step is to push our Spring Boot image to Dockerhub for later access. To do this I simply need to run the command
docker-compose push
The second step is to construct a slightly modified docker-compose.yaml file for use on the server.
Here is the docker-compose.yaml file I set up to run on the production server:
services: auction: image: joegregglu/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:
Here are some important changes to note in the configuration file:
The next step is to set up a server and install everything we need on it. The example commands below assume that I have set up an Ubuntu EC2 instance on Amazon Web Services that will serve as the server for our example.
One thing we need to watch out for when setting up the server is that we specify the correct architecture for the server. Since I built the auction image on a Mac laptop with the Arm architecture we need to sure to set up a server that uses the same Arm64 architecture.
Once the EC2 instance is up and running we can connect to it and start running the necessary setup commands.
We start by installing docker and docker-compose on the server:
sudo apt update sudo apt upgrade sudo apt install -y docker.io sudo systemctl start docker sudo systemctl enable docker sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose
Then we will need to copy our docker-compose.yaml and SQL dump file to the server. To do this, we disconnect from the server and run the scp
command from our local terminal:
scp -i <pem-file> auction.sql ubuntu@<server-address>:/home/ubuntu/ scp -i <pem-file> docker-compose.yaml ubuntu@<server-address>:/home/ubuntu/
where <pem-file>
is the name of the key file that AWS provided to us. Once those two files are in place we can connect back to the server and run the commands
sudo docker-compose pull sudo docker-compose up -d
to start our containers. At this point our containers will be up and running in the background on the server.