Spring boot and PostgreSQL in Docker Compose
Tags: springboot, docker, postgres, tutorial
Published: 2024-02-08 03:20:37

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


## 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](https://start.spring.io/). 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] (https://github.com/amol9372/docker-compose-tutorial) also.

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

```yaml
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
```

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

```Dockerfile
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"]
```

> 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)

```Dockerfile
FROM postgres:14.7
RUN chown -R postgres:postgres /docker-entrypoint-initdb.d/
```

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

```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');
```


Below is the `folder structure` for the entire project

```md
├── 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)
```
     
## 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


```yaml
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
```

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

```bash
POSTGRES_DB=employeedb
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_URL=host.docker.internal. # or use db(container-name)
POSTGRES_USER=postgres
HOST_URL=host.docker.internal
```

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

```yaml
volumes:
      - ./db-postgres/postgres-data:/var/lib/postgresql/data
      - ./db-postgres/create.sql:/docker-entrypoint-initdb.d/create.sql
```  

## Running Docker compose

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

```bash
docker-compose up
```

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

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

To stop the apps we can use the below command

```bash
docker-compose down
```

## Creating employee controller

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

### Employee Controller

```java
@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();
  }

}
```

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

```java
// 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;

}
```

Below is the Employee Repository

```java
@Repository
public interface EmployeeRepository extends JpaRepository<EEmployee, Integer> {
    Optional<EEmployee> findByEmail(String email);
}
```

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

```bash
docker-compose up
```

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

```bash
docker images
docker rmi <image-name-or-id>
```


Lets create an employee using `POST /api/employee`

```curl
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"
}'
``` 

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


```curl
curl --location 'localhost:8080/api/employee/johndoe@example.com'
```