Effortless Containerization: Deploying Spring Boot and MySQL with Docker and Docker Compose
Tags: docker, springboot, devops, webdev
Published: 2023-08-17 08:03:17

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8pncag13fwhou5sag5ai.png)

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](https://github.com/rajdip-b/springboot-docker-example) (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](https://start.spring.io/) and create a project using the following settings and dependencies.
![start.spring.io](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1yii7h18rh5r18ne8t7k.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4f82hpg6e7c889idbihq.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l8jteuaxzp06j9zb15im.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0peeb13qtfpbrlpktskm.png)
- `.env.local`: Used when using just docker.
![.env.local](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w19idnx3dil9fa9njrl9.png)

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](https://docs.docker.com/engine/install/) 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:
```bash
docker run --name springboot-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql
```
![docker run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0d8hdqv70bv4jmaa1wqi.png)

Here is a breakdown into what this command does:
- It creates us a container from the [mysql](https://hub.docker.com/_/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:
```bash
docker ps
```
![docker ps](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rt7t7eo883ucdwhny1av.png)

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:
```bash
curl http://localhost:8080/actuator/health
```

![actuator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5lw51trbup9qfrucxobn.png)

Now we are ready to finally dockerize the application!

## Dockerizing the application

We use **Dockerfile** to containerize any application using docker. [This](https://docs.docker.com/engine/reference/builder/) 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

```docker
# 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`.

![pom.xml](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gncfdf1gjs687zcftibx.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0hizd3wm5os5kgxp8fzy.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mxxs8m0ie8jx6cwuohh8.png)

Let's start by creating a docker network 
```bash
docker network create dummy-network
```
![docker network create](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2afvsmvu0v8tksywlerj.png)

Once done, you can verify this using
```bash
docker network ls
```
![docker network ls](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snkfkluwec9xbkhaockp.png)

Now, we need to migrate our MySQL container to this network. We do this by:
```bash
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:
```bash
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.
![host](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0mj6eknxosjyes486p9y.png)

We can bypass this by instructing docker to run the container directly on the host's network. This can be done by:
```bash
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.
![Host](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m7gkstapyre64xfsgr5h.png)

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:

```bash
docker build -t application:latest .
```

Next, run 

```bash
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 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.
 
```yaml
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:
```bash
docker stop springboot-test
docker stop temp
```

Now, we can fire up the entire infrastructure using:
```bash
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:

```bash
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.