DEV Community

Shitij Bhatnagar
Shitij Bhatnagar

Posted on

Simplified: Spring Boot with Docker (Part 3)

We covered the 'Multiple Services Instances' angle of Spring Boot and Docker in the Part 2 article of this series.

In this Part 3 article, we shall see how to manage dependencies between services, recovering from start failures, following a start up sequence through Docker Compose in an application that has multiple services. There are numerous examples of such an arrangement:

  • Microservice that talks to a database (another service)
  • Single Page application (SPA) that talks to microservice in background
  • Necessary service bindings (e.g. distributed logging)

Pre-requisites:

To experience the journey as outlined in the article, it is recommended to have the pre-requisites in place before getting on with the technical steps.

  • Reading: Basic awareness about Containerization & Docker
  • Tool: Installed Docker Desktop – contains multiple Docker services
  • Tool: Familiarity with Java (preferably 8 onwards), Spring Boot 3.4.x
  • Tool: Installed JDK 17 & Maven (build tool)
  • Hardware: Recommended to have at least 08 GB RAM (more the merrier)
  • Reading: Understood the Part 1 & Part 2 articles (Basics & Docker Compose)
  • Code: We shall use the 'Referral' Spring Boot application available at this Github Repo and Docker setup used in Part 1 & 2 article

Context:

We have a Spring Boot microservice called 'Referral'. This service helps persist & retrieve referral (any person) information in a back-end database - H2 (in memory) or external (PostgreSQL on-prem or container) and each of these 03 storage options can be enabled by choosing the active spring profile i.e. local (H2), dev (PostgreSQL on-prem) or dockerize (PostgreSQL on Docker container). Note that the default spring profile is local.

Let's look at the operations we can perform with the Referral application:

  • GET /referrals/status to get the status of the service (alternative is Actuator but that is not enabled right now)
  • GET /referrals to retrieve all the referrals
  • POST /referrals to add a referral [JSON request like { "givenName": "Marty" }]

Objectives we want to accomplish:

  1. Deploy the Referral application to Docker (think service)
  2. Keep multiple instances of the API / service (think replicas)
  3. Have request distribution to the Referral application instances (think another service i.e NGINX)
  4. Referral application to connect with PostgreSQL running in container (think another service i.e. PostgreSQL)

When using 'dockerize' spring profile, the database connectivity information & schema setups are not provided in the application properties but are defined externally in the Docker Compose YML (we shall touch on this later in the article)


Steps:

In order to meet the objectives, let's move one step at a time as there is a lot to accomplish here.


Understand Services & inter-dependencies

The traffic to 'Referral' application instances will channeled though the web server NGINX (similar approach to Part 2 article). The 'Referral' application instances shall integrate with PostgreSQL running in another container. So, we see at least 03 entities with their inter-dependencies as below:

  • NGINX (depends on Referral)
  • Referral (depends on PostgreSQL)
  • PostgreSQL (with necessary database schema)

What does 'Referral Controller' do?

Let's look at the below code:

package com.sb.mybatis.postgre.web;

import com.sb.mybatis.postgre.dto.ReferralDTO;
import com.sb.mybatis.postgre.service.ReferralService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/referrals")
public class ReferralController {

    @Autowired
    private ReferralService referralService;

    @GetMapping("/status")
    public String getStatus() {
        log.info("ReferralController.getStatus() is invoked");
        return "Referral Service is Up";
    }

    @GetMapping
    public List<ReferralDTO> getTransactions() throws Exception {
        log.info("ReferralController.getTransactions() is invoked");
        return referralService.findAllReferralRecords();
    }

    @PostMapping
    public ReferralDTO createTransaction(@Valid @RequestBody ReferralDTO referralDTO) throws Exception {
        log.info("ReferralController.createTransactions() is invoked");
        if(referralService.insertReferral(referralDTO) == 1) {
            //Success
            return referralDTO;
        }
        else throw new Exception("Error in creating new Referral record");
    }
}
Enter fullscreen mode Exit fullscreen mode

The ReferralController allows us to create/retrieve Referral records, provide service status and at the same time also add a simple message in the application logs that can be verified (would be referred later in the article).


Create Referral Executable Jar

First and foremost, please clone the Referral spring boot application locally and run the command mvn clean install to have the executable jar file ready (name like Referral-0.0.1-SNAPSHOT.jar).


Create Dockerfile

In order to create a Docker image for the Referral application, we first need to create a Dockerfile.

FROM openjdk:17
EXPOSE 8081
ARG JAR_FILE=Referral-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} Referral-0.0.1-SNAPSHOT.jar
ENTRYPOINT ["java","-Dspring.profiles.active=dockerize","-jar","/Referral-0.0.1-SNAPSHOT.jar"]
Enter fullscreen mode Exit fullscreen mode

Provision of the spring profile 'dockerize' in the ENTRYPOINT ensures this profile is picked up in the eventual image creation


Create 'Referral' Docker image

Using the Docker image, let's execute the command docker build -t sb-referral-service:V1 . to create the docker image for the Referral application.

A successful execution should look like below, indicating that the Docker image 'sb-referral-service:V1' is built.

Successful Image creation


Create NGINX.conf (web server configuration)

Similar to the Part 2 article, let's create a NGINX configuration file which should indicate that all requests to sb-referral-service should be channeled through the NGINX service.

user  nginx;

events {
    worker_connections   10;
}
http {
        server {
              listen 8000;
              location / {
                proxy_pass http://sb-referral-service:8081;
              }
        }
}
Enter fullscreen mode Exit fullscreen mode

Create PostgreSQL DB Schema creation script

As mentioned previously in the Step 'Understand Services & inter-dependencies', we would like to ensure that the PostgreSQL DB in Docker should have the necessary database schema. If this is not the case, the Referral application will not work because Spring Boot is not instructed to initiate schema creation in 'dockerize' mode/profile.

Let's create an SQL script by the name 'create-db.sql' with content as below:

create table if not exists T_REFERRAL
(
    id varchar(255) primary key,
    name varchar(255) NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

'create-db.sql' needs to be part of the PostgreSQL configuration in Docker Compose YML file


Define Service Orchestration via Docker Compose

Once we have the Referral image, database schema creation SQL script and the NGINX configuration files in place, the next step is to define how the different services will be associated, sequenced (in start up) and recover (in case of any dependent service failure). Let's have a look at the Docker Compose YML below:

services:
  nginx:
    image: nginx:latest
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - sb-referral-service
    ports:
      - '8000:8000'

  sb-referral-service:
    image: 'sb-referral-service:V1'
    restart: on-failure
    expose:
      - '8081'
    build:
      context: .
    deploy:
      replicas : 2
    depends_on:
      - db
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/test
      - SPRING_DATASOURCE_USERNAME=test
      - SPRING_DATASOURCE_PASSWORD=test
      - SPRING_JPA_HIBERNATE_DDL_AUTO=none

  db:
    image: 'postgres:13.1-alpine'
    container_name: db
    environment:
      - POSTGRES_USER=test
      - POSTGRES_PASSWORD=test
    volumes:
      - ./db:/var/lib/postgresql/data
      - ./create-db.sql:/docker-entrypoint-initdb.d/create_database.sql
Enter fullscreen mode Exit fullscreen mode

Looking at the YML file closely, we find 03 services:

  • NGINX: the web server, that will be setup by Docker by utilizing the NGINX.conf file and that has dependency on 'sb-referral-service'
  • sb-referral-service: the Referral application, that will have 02 instances (replicas), will be restarted by Docker if it's previous attempt was unsuccessful (restart: on-failure) and is dependent on 'db'
  • db: the PostgreSQL server, that will use the create-db.sql script and use the provided credentials for 'test' database

Notice the provision of the Spring Boot to database connectivity information in the 'environment' section under 'sb-referral-service' and the atypical spring datasource url as jdbc:postgresql://db:5432/test - that is because the database is running in a container


Starting the Services in Docker

To initiate creation of the services in Docker, we need to execute the command docker compose up (please ensure create-db.sql and nginx.conf files are also present in the same folder) and we should see a message like below:

docker compose up

Once the services are started successfully, we should be able to see them in Docker Desktop like below:

container view


Pre-Checks before consuming the services

Sometimes re-starts can be noticed in the upstream services (in this case, sb-referral-service) if the downstream services (in this case, db) are still getting setup while the upstream is trying to connect to the downstream. So, instead of rushing to consume the Referral application after seeing 05 containers visible in the Docker Desktop Dashboard, we can do some pre-checks also.

So, before we attempt to start using the API/end points of Referral application at the URL http://localhost:8000/referrals (note the port 8000 is of NGINX), we can additionally verify the following messages from the command prompt / log messages displayed by 'docker compose up' command:

Successful PostgreSQL setup:

db ready

Successful Referral application setup:

app ready

Because the 'depends on' (in Docker Compose YML) only waits for the other container to be up (and not for its contained application to be fully running), it is advisable to additionally verify the applications/services before consuming them.


Consuming the 'Referral' application

Now that the services are running in Docker, let's try consuming them, this time from Postman.

When we execute GET http://localhost:8000/referrals/status, we should get a successful message about service status, like below.

app service status

When we execute GET http://localhost:8000/referrals, we should receive zero Referral records, because none exist, like below.

empty referrals

When we execute POST http://localhost:8000/referrals, we should see a success message with the Referral information, like below.

create referral success

Now, again when we execute GET http://localhost:8000/referrals to see if we see the newly created Referral in the fetch operation, we receive the Referral record(s), like below.

list referrals

In addition to the above, let's also verify the Referral application logs. For the same, we can go to the individual sb-referral-service instances/containers and go to Logs option, we should see below (based on the above success).

service log 1

service log 2

Note that as per the above two logs, Referral services instances received different requests (from Postman), which also proves that NGINX is rotating the requests between the instances successfully


Destroy the containers / services

As our work finishes, we can finish the containers using the command docker compose down and we should see a confirmation, like below.

docker compose down


Summary

In this article, we saw how Docker (and Docker Compose) allowed us to comfortably build a service orchestration that is typically only possible in a test environment - on our development machine and how we can use it - covering multiple services (including database), service to service communication, service start up sequencing and recovery.

Would appreciate any feedback for improvement on the approach and content.

References:

https://stackoverflow.com/questions/52699899/depends-on-doesnt-wait-for-another-service-in-docker-compose-1-22-0

Top comments (0)