DEV Community

Amol Singh
Amol Singh

Posted on

Spring boot and PostgreSQL in Docker Compose

Introduction

In this tutorial we will be running a Spring boot application & open source database PostreSql. We will be using docker compose to run multiple containers at once.

docker compose flow

Requirements

  • Docker (docker-compose)
  • Jdk
  • Maven
  • IDE (Intellij or VScode)

Spring boot application

In this segment we will create a basic Spring boot app from Spring Initializer. Add the below dependencies:

  1. Spring Data JPA
  2. Spring Web
  3. PostgreSql driver

Download the generated zip file & load it into your IDE. You can use this github project also.

We need to setup Database configuration in application.yaml file.

spring:
  datasource:
    url: jdbc:postgresql://${POSTGRES_URL}:5432/${POSTGRES_DB}
    username: ${POSTGRES_USER}
    password: ${POSTGRES_PASSWORD}
    driver-class-name: org.postgresql.Driver
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
Enter fullscreen mode Exit fullscreen mode

We are using variables for Database here. We will externalize these variables in docker-compose.yaml file.

If we try to run the application, it will give us an error since there is no database setup.

Building the Application Dockerfile

Lets create a Dockerfile in root of spring boot project

The contents of the file will be something like

FROM amazoncorretto:17-alpine-jdk
# Setting work directory
WORKDIR /app  
# Install Maven
RUN apk add --no-cache maven

# Copy your project files and build the project
COPY . .
RUN mvn clean install
ENTRYPOINT ["java","-jar","target/docker-postgres-spring-boot-1.0.jar"]
Enter fullscreen mode Exit fullscreen mode

We are setting up maven so that it automatically clean installs the application which creates jar in target. With this, we do not need to copy jar file from target every time.

Postgres Dockerfile

Lets create Dockerfie for PosgreSql. We can create all this configuration in docker-compose.yaml file but it becomes really easy once we segregate.

On the root, lets create a folder db-postgres which will contain

  • Dockerfile
  • data.sql file
  • postgres-data (folder for docker volume)
FROM postgres:14.7
RUN chown -R postgres:postgres /docker-entrypoint-initdb.d/
Enter fullscreen mode Exit fullscreen mode

We will be creating some tables and adding some reference data as a part of container initialization in create.sql

CREATE SCHEMA IF NOT EXISTS "public";

CREATE TABLE "public".employee (
    id SERIAL PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100),
    department VARCHAR(100),
    position VARCHAR(100),
    salary DECIMAL(10, 2),
    hire_date DATE
);

-- Insert sample records into the 'employee' table
INSERT INTO employee (first_name, last_name, email, department, position, salary, hire_date) 
VALUES 
('John', 'Doe', 'johndoe@example.com', 'Engineering', 'Software Developer', 75000.00, '2020-01-15'),
('Jane', 'Smith', 'janesmith@example.com', 'Marketing', 'Marketing Manager', 65000.00, '2019-08-01'),
('Emily', 'Jones', 'emilyjones@example.com', 'Human Resources', 'HR Coordinator', 55000.00, '2021-05-23');
Enter fullscreen mode Exit fullscreen mode

Below is the folder structure for the entire project

├── docker-compose.yaml
├── db-postgres/
|   ├── Dockerfile 
│   ├── data.sql (DDL file)
│   └── postgres-data/ (docker volume)
└── docker-postgres-spring-boot/ (application root)
    ├── Dockerfile
    ├── pom.xml
    └── src/
        ├── main/
        │   ├── java/
        │   │   └── your.package/
        │   ├── resources/
        │   │   ├── application.yml
        └── ... (other project files)
Enter fullscreen mode Exit fullscreen mode

Docker compose file

Lets create the final docker-compose.yaml file. There are 2 services in the docker compose file

  • Spring boot app
  • Postgres DB
version: "3.8"

services:
  db:
    build:
      context: ./db-postgres
      dockerfile: Dockerfile
    ports:
      - "5432:5432"
    volumes:
      - ./db-postgres/postgres-data:/var/lib/postgresql/data
      - ./db-postgres/create.sql:/docker-entrypoint-initdb.d/create.sql
    env_file:
      - .env

  app:
    depends_on:
      - db
    build:
      context: ./docker-postgres-spring-boot
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    env_file:
      - .env
Enter fullscreen mode Exit fullscreen mode

We are not passing individual environment variables, rather we are using --env-file argument in docker-compose.yaml

create a file .env at the root (same directory as docker-compose.yaml file). Add the following variables.

POSTGRES_DB=employeedb
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_URL=host.docker.internal. # or use db(container-name)
POSTGRES_USER=postgres
HOST_URL=host.docker.internal
Enter fullscreen mode Exit fullscreen mode

POSTGRES_URL=host.docker.internal is an internal docker host which is used.
MacOS - if nothing works use host.docker.internal
Windows/linux/WSL - We can use container-name - db

The docker-compose.yaml is located at the root.

Lets go through the file now

Spring boot app

  • The Spring boot app uses a Dockerfile located on the root of the spring boot project folder specified by context: ./docker-postgres-spring-boot
  • We have exposed PORT 8080 on the host machine
  • We are using --env-file attribute to declare environment variables

Postgres DB

  • The postgres uses Dockerfile inside postgres-db folder specified by context
  • We are using volumes to have the data persisted on every restart of the containers
  • With volume we are placing create.sql in the container & using postgres-data as mount folder on host machine. This folder will contain data.
volumes:
      - ./db-postgres/postgres-data:/var/lib/postgresql/data
      - ./db-postgres/create.sql:/docker-entrypoint-initdb.d/create.sql
Enter fullscreen mode Exit fullscreen mode

Running Docker compose

We will be running the services in docker-compose using the command

docker-compose up
Enter fullscreen mode Exit fullscreen mode

Check the logs. If everything runs fine, we should see logs like this

db-1   | 2024-02-08 01:49:44.444 UTC [1] LOG:  database system is ready to accept connections
app-1  | 2024-02-08T01:49:47.090Z  INFO 1 --- [           main] .p.d.DockerPostgresSpringBootApplication : Started DockerPostgresSpringBootApplication in 2.949 seconds (process running for 3.513)
Enter fullscreen mode Exit fullscreen mode

To stop the apps we can use the below command

docker-compose down
Enter fullscreen mode Exit fullscreen mode

Creating employee controller

We will create an API to get data from the employee table which was created as part of containerization.

Employee Controller

@RestController
@RequestMapping("employee")
public class EmployeeController {

  @Autowired
  EmployeeRepository employeeRepository;

  @GetMapping("{email}")
  public ResponseEntity<?> authenticate(@PathVariable String email) {

    return ResponseEntity.ok(employeeRepository.findByEmail(email));
  }

  @PostMapping
  public ResponseEntity<?> register(@RequestBody Employee request){
    employeeRepository.save(EEmployee.builder()
            .email(request.getEmail())
            .firstName(request.getFirstName())
            .lastName(request.getLastName())
            .salary(request.getSalary())
            .department(request.getDepartment())
            .position(request.getPosition())
            .hireDate(LocalDate.now())
            .build());
    return ResponseEntity.created(URI.create("/api/employee")).build();
  }

}
Enter fullscreen mode Exit fullscreen mode

We have created employee controller with 2 endpoint:
GET /api/employee/email
POST /api/employee

Creating Employee entity and Repository

Lets create a simple EEmployee class entity

// Lombok annotations
@Table(name = "employee")
public class EEmployee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    int id;

    @Column(name = "first_name", nullable = false, length = 50)
    String firstName;

    @Column(name = "last_name", nullable = false, length = 50)
    String lastName;

    @Column(name = "email", nullable = false, length = 100)
    String email;

    @Column(name = "department", nullable = false, length = 100)
    String department;

    @Column(name = "position", nullable = false, length = 100)
    String position;

    @Column(name = "salary", nullable = false)
    double salary;

    @Column(name = "hire_date", nullable = false)
    LocalDate hireDate;

}
Enter fullscreen mode Exit fullscreen mode

Below is the Employee Repository

@Repository
public interface EmployeeRepository extends JpaRepository<EEmployee, Integer> {
    Optional<EEmployee> findByEmail(String email);
}
Enter fullscreen mode Exit fullscreen mode

For simplicity purposes we are calling repository directly from the controller

Running Docker compose again

Lets run docker compose with updated code. We do not need to copy target file to docker context.

docker-compose up
Enter fullscreen mode Exit fullscreen mode

We can also remove the docker images if code is not updating

docker images
docker rmi <image-name-or-id>
Enter fullscreen mode Exit fullscreen mode

Lets create an employee using POST /api/employee

curl -X POST http://localhost:8080/api/employee \
-H "Content-Type: application/json" \
-d '{
    "firstName": "Mike",
    "lastName": "Thomas",
    "email": "mike@thomas.com",
    "department": "Sales",
    "position": "Software Developer",
    "salary": 123000.0,
    "hireDate": "2019-01-15"
}'
Enter fullscreen mode Exit fullscreen mode

Lets try to fetch employee using GET /api/employee/{email}

curl --location 'localhost:8080/api/employee/johndoe@example.com'
Enter fullscreen mode Exit fullscreen mode

Top comments (0)