Effortless Containerization: Deploying Spring Boot and MySQL with Docker and Docker Compose
Rajdip Bhattacharya
Posted on August 17, 2023
Greetings!
In the increasing complexity of software development, there is a general shift from manual configuration to automation. Be it development, or deployment, seamless development and integration is the word of the town.
Hence, in this blog, I plan to crack open a critical aspect of DevOps: Containerization.
What is Containerization?
Before we dive into that, first let us understand the old school way of doing it. Make a supposition that you are developing an application. You know every inch of this application. Be it the databases or the environments, you have everything set up. Now consider that this application is done with development. You now want to host it someplace. For instance, let's say you decided to use Amazon EC2 for its ease of use. Now let's look at the steps that you would perform to get this application running (at least).
- Clone the application into the EC2 instance
- Install required dependencies (Java, NodeJS, etc.)
- Set up the required environmental variables
- Test the application for bugs or errors.
While this might look trivial, it becomes a pain when you have to do it over and over again, for all of your applications. There is always a chance that some OS feature would break your application, some dependency won't be installed, some environmental variable might not be configured. It becomes extremely difficult to debug such applications. Here is where containerization comes to our rescue.
A container includes everything needed for an application to run: the code, runtime, system libraries, and settings. This self-contained unit ensures that the application behaves consistently regardless of the environment it's deployed in. Containers are lightweight, portable, and can be easily moved between different host systems or cloud platforms without significant modifications.
Key features and benefits of containerization include:
Isolation: Containers are isolated from each other and from the host system, preventing conflicts between dependencies and runtime environments.
Consistency: Containers ensure that an application behaves the same way in every environment, reducing the "it works on my machine" problem.
Portability: Containers can be moved between different systems or cloud platforms with minimal effort, making application deployment and migration easier.
Resource Efficiency: Containers share the host OS kernel, resulting in lower overhead compared to traditional virtualization.
Scalability: Containers can be rapidly scaled up or down to accommodate varying workloads and demands.
Version Control: Container images can be versioned, allowing teams to track changes and roll back to previous versions.
DevOps Enablement: Containers facilitate DevOps practices by enabling continuous integration, continuous delivery, and automated deployment.
Roadmap
In this blog, we would be doing the following things.
- Create a simple SpringBoot application to manage a
Person
entity - Set up MySQL using Docker
- Create a Docker Image of our application
- Develop a run-and-deploy of the entire infrastructure using Docker Compose
To follow along, clone this repository (also leave a star maybe?)
So, let's get started!
Creating the SpringBoot application
We would start with our SpringBoot application. Head over to Spring Initializr and create a project using the following settings and dependencies.
To summarize, we are using:
- Lombok: For annotation based POJOs.
- Spring Web: For our web application.
- Spring Data JPA: For Hibernate ORM
- MySQL Driver: Enables our application to talk to a MySQL database.
- Spring Boot Actuator: For health checks
Once you are satisfied, generate the project, extract it, and open it with your favourite code editor.
We would be creating the following classes:
-
Person
: Base entity for holding the person. -
PersonPayload
: Contains the request body for a Person object. -
PersonDTO
: Contains the DTO of the person object when the API returns data. -
PersonService
: Contains business interface for managing persons. -
PersonServiceImpl
: Contains the implementation ofPersonService
. -
PersonController
: Exposes the endpoints for managing Persons. -
PersonRepository
: Enables us to use JPA
Here are a list of endpoints we would be developing in PersonController
:
-
POST /api/person/
: Creates a person -
PUT /api/person/{personId}
: Updates a person with the given data -
GET /api/person/all?page=<page_index>&size=<page_size>
: Gets the list of all persons on the database.page
andsize
helps us to control the index of a page and number of items in a page in pagination. -
GET /api/person/{personId}
: Gets a person by their ID -
DELETE /api/person/{personId}
: Delete a person by their ID
Our final folder structure should look something like this:
Since this blog is focussed on getting Docker set up, I would be skipping the code explanation in here. You can always clone the repository and check the code.
Let's go through the application.properties
file
As you can see, I have injected environmental variables. This would allow us to resolve the values at runtime. It gives us the flexibility to configure our application without actually touching any of the code. Any change that you would like to make to the environments, all you need to do is tweak the values as per your wish in the system's environment.
I will be using a .env
file to feed the values into the application. Nearly every IDE has the support for doing so. In case you can't figure out how to include a .env
file into your execution environment, you can try setting those values in the environment of your OS. Alternatively, you can let that untouched, since all the keys have a default value assigned to them.
-
.env.docker
: Used when deployed using docker compose. -
.env.local
: Used when using just docker.
Now, we have our application ready. Before we are actually able to run it, we need to launch MySQL, which we will do next.
Launching MySQL using Docker
To begin with this step, first, make sure that you have docker installed. This article provides an excellent guide in letting you set up docker. Once it is set up, we are ready to move forward.
We will be launching a MySQL docker container using this command:
docker run --name springboot-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql
Here is a breakdown into what this command does:
- It creates us a container from the mysql docker image
- It assigns a name to that container using
--name springboot-test
- It exposes the standard mysql port of the docker container to the host's network using
--p 3306:3306
- It sets the
root
password of the docker image toroot
using-e MYSQL_ROOT_PASSWORD=root
- Lastly, it tells the container to run in detached mode, meaning, it won't be attached to the console where we are writing this command using the
-d
flag
With that, we have a brand new mysql container up and running which you can check using:
docker ps
At this point of time, we are ready to launch our springboot application. Go to the root of the project, and run
- If you have maven installed: ```bash
mvn spring-boot:run
- If you don't have maven:
```bash
mvnw spring-boot:run
You can verify that the application is running using:
curl http://localhost:8080/actuator/health
Now we are ready to finally dockerize the application!
Dockerizing the application
We use Dockerfile to containerize any application using docker. This article provides all that you need to know to get started. For starters, I'll run down through the docker image that I'm creating.
- Go to the project's root
- Create a file named
Dockerfile
- Paste the following content in the file
# The base image on which we would build our image
FROM openjdk:18-jdk-alpine
# Install curl and maven
RUN apk --no-cache add curl maven
# Set environment variables
ENV DB_HOST=${DB_HOST}
ENV DB_NAME=${DB_NAME}
ENV DB_USER=${DB_USER}
ENV DB_PASS=${DB_PASS}
# Expose port 8080
EXPOSE 8080
# Set the working directory
WORKDIR /app
# Copy the pom.xml file to the working directory
COPY pom.xml .
# Resolve the dependencies in the pom.xml file
RUN mvn dependency:resolve
# Copy the source code to the working directory
COPY src src
# Build the project
RUN mvn package -DskipTests
# Run the application
ENTRYPOINT ["java", "-jar", "target/application.jar"]
Notice that I'm still not hard coding the environmental variables. Also, note that, the last line mentions using the command java -jar target/application.jar
to launch the container. For this to happen, we need to first set the build name to application
in the pom.xml
.
To optimize the docker build process, I have first copied the pom.xml
and then resolved the dependencies before actually copying our soruce code. This is done with the purpose of reducing the number of layers docker rebuild during its build process. Source code is bound to change often. Putting that at the very top would mean all the subsequent layers would be rebuilt.
Docker networking
Before we get started with running the application, let's first get a few points right about networking in docker. When we run a docker image, the container boots up into a separate docker network that works in isolation to our host network and other docker containers. Hence, container A can't ping container B if they are running on different networks. In our case, we would be running the springboot application and MySQL database. So if we let them run in different networks, our containers won't be able to intercommunicate.
There are two ways to address this issue:
- Create a custom network and attaching our containers to that network.
- Attaching the containers directly to the host network.
Using custom network
Let's start by creating a docker network
docker network create dummy-network
Once done, you can verify this using
docker network ls
Now, we need to migrate our MySQL container to this network. We do this by:
docker network disconnect bridge springboot-test
docker network connect dummy-network springboot-test
Now, we can verify that these commands work by inspecting the Containers
section in the output of this command:
docker network inspect dummy network
Using host network
Recall that we used the flag -p 3306:3306
while creating our MySQL container. This flag creates a channel in the network of our MySQL container that allows us to communicate with the container's 3306
port via our hosts 3306
port.
We can bypass this by instructing docker to run the container directly on the host's network. This can be done by:
docker run --name springboot-test --network host -e MYSQL_ROOT_PASSWORD=root -d mysql
Notice that I replaced the -p
flag with the --network
flag. When we are using the host
network driver, port mappings are neglected by docker.
Now that we know the fixes, let's move towards making the application work.
Running the SpringBoot application
We have created our Dockerfile in the previous sections. Now, first, we need to create a docker image out of that file. To do this, go to the root directory of the project and run:
docker build -t application:latest .
Next, run
docker run --name temp --rm -p 8080:8080 --env-file .env.local --network dummy-network application:latest
This command will do the following:
-
--name temp
will set the name of the container astemp
-
--rm
will remove the container once it's stopped -
-p 8080:8080
will map the container's port 8080 to the host's port 8080 -
--env-file .env.local
will read the environments from the.env.local
file -
--network dummy-network
will associate the container todummy-network
- Lastly, it will launch the docker container
In case you get any error stating that the DB connection fail, I would like to point you to the .env.local
file. In there, we have a key called DB_HOST
with the value set as springboot-test
. This is the name we used when launching the MySQL docker container. The above command will only work when these conditions are satisfied:
- The MySQL container is named
springboot-test
- Both the database and the application are in the same network.
Alternatively, if you want to use some other name for the MySQL container, you can should the name in the .env.local
file aswell.
Now that we have everything up and running, we can verify our network again using the docker network inspect dummy-network
command.
Using docker compose
No doubt that most of you have felt that this is too much of configuration. Yes, configuration comes as the cost of making applications reliable and secure. But don't feel demotivated, docker compose is here to rescue!
docker compose is a plugin that reads deployment configurations from a file (typically named docker-compose.yaml) and deploys the entire infrastructure at one click!
For doing this, let us first create the docker-compose.yaml file in the root directory of the project.
Then, paste the following into the file.
version: '3.8'
services:
mysql:
container_name: mysql
image: mysql
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=admin1234
networks:
- stack
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-padmin1234"]
interval: 30s
timeout: 10s
retries: 3
application:
container_name: application
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
env_file:
- .env.docker
networks:
- stack
depends_on:
mysql:
condition: service_healthy
networks:
stack:
name: stack
driver: bridge
As you can see, we have created a network named stack
. We have created two services - application
and mysql
. Both of these services come under the stack
network. In the .env.docker
file, I have set the DB_HOST
to mysql
. This name corresponds to the name of the service. In the docker file, I have added a dependency of mysql
in application
service. This means that the application
service wont start before the mysql
service reaches the service_healthy
state.
Once done, shut down your previous containers using:
docker stop springboot-test
docker stop temp
Now, we can fire up the entire infrastructure using:
docker compose up
This command will take some time to start up. A few flags that might come in handy:
-
-d
: Starts in detached mode -
--build
: Rebuilds the images. Useful when you have made any changes to the source code.
When you are done playing around with, you can shut down the entire thing by using:
docker compose down
Conclusion
So that was all about using docker to make your lives easier. I hope you have quiet a few tricks by now. Feel free to leave a comment in case you find something off.
Posted on August 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 17, 2023