In this tutorial, you will learn to design and deploy the multi-container based Spring Boot application using Docker compose.
To learn more about Docker, please check Docker Tutorials page.
Overview
In the previous tutorial, we learned how to Dockerize a simple Spring Boot-based application. As it was a small application we have handled each container individually using multiple docker commands. But in a real-world scenario, while developing production-grade software, their single host service interacts with many dependent services in Docker containers. So, it is practically impossible to manage each container manually via docker commands. There is a need to manage containers like a coherent whole. Here Docker Compose comes to the rescue.
Docker Compose is a very powerful tool that allows you to very quickly deploy applications with complex architecture. It is used to manage multiple containers that are part of the same application service. This tool offers the same capabilities as Docker but allows you to work with more complex applications.
In this article, I’ll show you how to host an application in a Docker container and smoothly manage them using docker-compose.
Preparation:
- JDK 1.8
- Intellij Idea for IDE
- Maven
- SpringBoot Application
- Lombok plugin
- Docker Desktop
In this blog, I’ll host an application in a Docker container, which consists of two inter-dependent containers:
- A container for Spring Boot Application
- A PostgreSQL database container
In the end, we will manage the interplay between two containers using the single docker-compose file.
Step 1: Creating a Basic Spring Boot Application
You can use the Spring Initializr page to bootstrap your project. After importing the project into IDE, we will be creating the following sub-packages inside our main package ‘com.appsdeveloperblogs.dockercomposetutorial‘ :
- Controller: This package contains class ‘ItemController‘ with filename ‘ItemController.java’
- Entity: This package contains item entity POJO structure with filename ‘Item.java’
- Repository: This package contains JpaRepository interface implementation to perform CRUD operation with Postgres DB.
- Service: This package contains the service class with the filename ‘service.java’.
In the end, the final project structure will look like this:
In the following sections, we will learn about each section in detail.
Maven Dependencies
The complete pom.xml file will be as below −
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> </parent> <groupId>com.appsdeveloperblog.DockerComposeTutorial</groupId> <artifactId>Spring-boot-DockerCompose</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringBoot Docker Compose Tutorial</name> <description>SpringBoot Docker Compose Tutorial</description> <properties> <java.version>11</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.18</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Entity Class
package com.appsdeveloperblogs.dockercomposetutorial.entity; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class Item { @Id @GeneratedValue private int id; private String name; }
Controller Class
In this class, we will be annotating it with @RestController annotation. Hence it will handle our web requests like GET /items for retrieving item lists from Postgres DB. The complete code for the ItemController.java file is as follow −
package com.appsdeveloperblogs.dockercomposetutorial.controller; import java.util.List; import com.appsdeveloperblogs.dockercomposetutorial.service.ItemService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.appsdeveloperblogs.dockercomposetutorial.entity.Item; @RestController public class ItemController { @Autowired private ItemService itemService; @GetMapping("/items") public List<Item> items() { return itemService.findAll(); } }
Repository Class
This class extends the JpaRepository interface, as we are using PostgreSQL DB for persisting data. Hence, JpaRepository will by default provides us with generic methods like save(), findAll(), insert(), etc.
The complete code for the UserRepository.java file is as follow −
package com.appsdeveloperblogs.dockercomposetutorial.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.appsdeveloperblogs.dockercomposetutorial.entity.Item; public interface ItemRepository extends JpaRepository<Item, Integer>{ }
Service Class
The class is annotated with the @Service annotation, here we write the business logic to store, retrieve and update the item. We will insert three test records in our Postgres tables at the start of the application.
The complete code for the ItemService.java file is as follow −
package com.appsdeveloperblogs.dockercomposetutorial.service; import java.util.List; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.appsdeveloperblogs.dockercomposetutorial.entity.Item; import com.appsdeveloperblogs.dockercomposetutorial.repository.ItemRepository; @Service public class ItemService { @Autowired private ItemRepository itemRepository; @Transactional @PostConstruct public void init() { itemRepository.save(new Item(1, "John")); itemRepository.save(new Item(2, "Alex")); itemRepository.save(new Item(3, "Sandra")); } public List<Item> findAll() { return itemRepository.findAll(); } }
Step 2: Creating a Docker file
Firstly, we create a file with the name Dockerfile at the root of our project structure with the contents shown below. This Dockerfile instructs docker on which format the application image needs to be created.
The content of Dockerfile is as follow:
FROM openjdk:11 ADD target/*.jar myapplication ENTRYPOINT ["java", "-jar","myapplication"] EXPOSE 8080
Step 3: Creating Docker Compose file
Eventually, we will create docker-compose.yml file.
Typically, an application consists of several components that depend on each other but can be run in isolation on different machines. For example, in our current Spring Boot application, we provide REST API with CRUD operation in PostgreSQL DB. Here we have two containers – the one that ensures the operation of the site via REST API, and the other one is responsible for running the database. For such usecase, it is convenient to use docker-compose – where we can handle all services in a single file.
The docker-compose.yml file is shown below:
version: '3' services: myspringapp: image: myapplication build: . ports: - "8080:8080" restart: always depends_on: - mypostgres environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://mypostgres:5432/mydb - SPRING_DATASOURCE_USERNAME=postgres - SPRING_DATASOURCE_PASSWORD=password - SPRING_JPA_HIBERNATE_DDL_AUTO=create mypostgres: image: postgres restart: always ports: - "5432:5432" environment: - POSTGRES_PASSWORD=password - POSTGRES_USER=postgres - POSTGRES_DB=mydb
Understanding the docker-compose.yml
Let’s understand the docker-compose file in detail.
Version:
This specifies to Docker Compose which version you want to use for composing the file. In our case, we will use version 3, which is currently the most used version.
Services:
The set of containers that need to be controlled by the docker-compose needs to be defined under the Services argument. We have defined two services with the names myspringapp and mypostgres respectively for two containers in our application. Then, you need to describe your container with various other arguments as required.
- image: It allows us to define the Docker image name we want to use.
- build: This specifies the location of our Dockerfile, and
.
represents the directory where the docker file is located. - ports: This allows us to tell Docker Compose that we want to expose a port from our host machine to our container, and thus make it accessible from the outside.
- restart: The containers being by definition single-process, if this one encounters a fatal error, it can be brought to stop. In our case, if the Postgres SQL server stops, it will restart automatically thanks to the argument restart: always.
- depends_on: This allows to specify in which order to start and stop a container. For example, the Postgres SQL container needs to be up before the Spring application starts.
- environment: The clause allows us to set up an environment variable in the container for individual containers to run.
Launch of the project
Now that the project is built, it’s time to launch it. This step of our work corresponds to the step when the command docker run
is executed while working on individual containers without docker-compose.
As mentioned in the docker file, the server exposed port 8080 to serve client requests. Therefore, if you go to the browser at the address http://localhost:8080/items, it will display the data retrieved from the Postgres SQL database.
Let’s run some commands on the terminal.
First, we need to build our project using the standard maven command:
mvn clean install
Next, we need to create a containerized image for our application using a docker-compose command.
docker-compose build
it should display output as below:
Now we need to run all our containers using the single command as below.
docker-compose up
If docker already has dependent images present locally then it will start directly else first it pulls images for the docker hub. As below, docker is pulling Postgres images from the internet which docker-compose could not find it locally. This is a single time activity.
If everything compiles successfully, the output will display the success message as below:
ation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] myspringapp_1 | 2021-11-13 11:56:30.070 INFO 1 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory f or persistence unit 'default' myspringapp_1 | 2021-11-13 11:56:30.888 WARN 1 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning myspringapp_1 | 2021-11-13 11:56:31.462 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' myspringapp_1 | 2021-11-13 11:56:31.484 INFO 1 --- [ main] c.a.d.DemoApplication : Started DemoApplication in 8.107 secon ds (JVM running for 8.849)
Now, all the containers are up and running, we’ll try accessing the RESTful service using Postman or browser.
We’ll access the endpoint as:
http://localhost:8080/items
As expected, it will display a page with a list of item records fetched from Postgres SQL DB.
Stopping Containers
In addition, to stop all containers, we can use the following commands.
docker-compose down
Conclusion
In this tutorial, we learned how docker-compose can help in running complex applications in distributed systems with minimal management. In other words, by using simple docker-compose.yml instructions you can build, scale, heal and run any number of containers easily. I hope this tutorial was helpful to you.