DEV Community

Cover image for Effortless Containerization: Deploying Spring Boot and MySQL with Docker and Docker Compose
Rajdip Bhattacharya
Rajdip Bhattacharya

Posted on

Effortless Containerization: Deploying Spring Boot and MySQL with Docker and Docker Compose

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

Architecture

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.
start.spring.io

To summarize, we are using:

  1. Lombok: For annotation based POJOs.
  2. Spring Web: For our web application.
  3. Spring Data JPA: For Hibernate ORM
  4. MySQL Driver: Enables our application to talk to a MySQL database.
  5. 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:

  1. Person: Base entity for holding the person.
  2. PersonPayload: Contains the request body for a Person object.
  3. PersonDTO: Contains the DTO of the person object when the API returns data.
  4. PersonService: Contains business interface for managing persons.
  5. PersonServiceImpl: Contains the implementation of PersonService.
  6. PersonController: Exposes the endpoints for managing Persons.
  7. 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 and size 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:

folder structure

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

application.properties

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.docker
  • .env.local: Used when using just docker. .env.local

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
Enter fullscreen mode Exit fullscreen mode

docker run

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 to root 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
Enter fullscreen mode Exit fullscreen mode

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:
mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode
  • If you don't have maven:
mvnw spring-boot:run
Enter fullscreen mode Exit fullscreen mode

You can verify that the application is running using:

curl http://localhost:8080/actuator/health
Enter fullscreen mode Exit fullscreen mode

actuator

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"]
Enter fullscreen mode Exit fullscreen mode

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.

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.
Networks

There are two ways to address this issue:

  1. Create a custom network and attaching our containers to that network.
  2. Attaching the containers directly to the host network.

Using custom network

Custom Docker network

Let's start by creating a docker network

docker network create dummy-network
Enter fullscreen mode Exit fullscreen mode

docker network create

Once done, you can verify this using

docker network ls
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now, we can verify that these commands work by inspecting the Containers section in the output of this command:

docker network inspect dummy network
Enter fullscreen mode Exit fullscreen mode

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.
host

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
Enter fullscreen mode Exit fullscreen mode

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.
Host

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 .
Enter fullscreen mode Exit fullscreen mode

Next, run

docker run --name temp --rm -p 8080:8080 --env-file .env.local --network dummy-network  application:latest
Enter fullscreen mode Exit fullscreen mode

This command will do the following:

  • --name temp will set the name of the container as temp
  • --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 to dummy-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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now, we can fire up the entire infrastructure using:

docker compose up
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)