How to Dockerize a Spring Boot App with PostgreSQL
Tags: docker, springboot, containers
Published: 2023-04-19 00:25:31

---
title: How to Dockerize a Spring Boot App with PostgreSQL
published: true
date: 2023-04-18 23:43:42 UTC
tags: docker, springboot, containers
canonical_url: https://potatoesblog.tech/how-to-dockerize-a-spring-boot-app-with-postgresql
---

In this article, I will show you how to use Docker with SpringBoot and PostgreSQL, to follow up you should have a basic understanding of what is `Docker`, `Maven` installed, experience with `Spring Boot` and an IDE of your choice.

**<mark>Notice</mark>** <mark>: If you already have a Spring Boot application you can skip sections 1 and 2 and go directly to section 3 and follow along.</mark>

[Here you can find the code and other resources used in this demonstration (github.com)](https://github.com/GHamza-Dev/docker-spring-boot-demo)

### 1 - What we will build?

For demo purposes, we will create a simple recipe management app with two entities: `Chef` and `Recipe`.

![uml: class diagram - recipe management app](https://cdn.hashnode.com/res/hashnode/image/upload/v1681742589211/e58f21ec-7bf0-44b4-90aa-1120da10ebc2.png)

### 2 - Create a spring boot application

For this, you can either use [spring initializr](https://start.spring.io/) or spring initializr plugin if you are using `IntelliJ idea`. and select the following dependencies: `Spring Web`, `PostgreSQL Driver` and of course `Spring Data JPA` and make sure to choose `maven` as a project manager.

![spring initializr website](https://cdn.hashnode.com/res/hashnode/image/upload/v1681741843478/73880dc6-7e0f-47dd-b98d-c690e280b51e.png)

After installing the project extract it and open it with your favorite IDE.

as mentioned in the first section we will create two entities `Chef` and `Recipe`. as follows:

- Chef:

```java

@Entity
public class Chef {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "chef")
    private List<Recipe> recipes;

    public Chef() {
    }

    public Chef(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Chef(String name) {
        this.name = name;
    }

    // ADD GETTERS AND SETTER...
}

```

- Recipe

```java
@Entity
public class Recipe {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 100)
    private String name;
    private String description;

    @ManyToOne
    @JsonBackReference
    private Chef chef;

    public Recipe() {
    }

    public Recipe(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public Recipe(Long id, String name, String description) {
        this.id = id;
        this.name = name;
        this.description = description;
    }

    // GETTERS AND SETTERS...
}

```

We need also to create `ChefController`, `ChefService`, `ChefRepository` and `RecipeRepository`.

**ChefService:** has three basic methods: `createChefWithRecipes` which takes as a parameter a chef and list of recipes, `getChefs` to get the list of chefs, and finally `getNumberOfChefs` returns the number of chefs in the database.

```java
@Service
public class ChefService {
    private ChefRepository chefRepository;
    private RecipeRepository recipeRepository;

    public ChefService(ChefRepository chefRepository, RecipeRepository recipeRepository) {
        this.chefRepository = chefRepository;
        this.recipeRepository = recipeRepository;
    }

    @Transactional
    public Chef createChefWithRecipes(Chef chef,List<Recipe> recipes){

        recipes.stream().forEach(recipe -> recipe.setChef(chef));

        this.chefRepository.save(chef);
        this.recipeRepository.saveAll(recipes);

        return chef;
    }

    public List<Chef> getChefs(){
        System.out.println(this.chefRepository.findAll());
        return this.chefRepository.findAll();
    }

    public Long getNumberOfChefs(){
        return this.chefRepository.count();
    }
}

```

**ChefController:** it has only one method which returns a list of chefs

```java
@RestController
@RequestMapping("/api/chefs")
public class ChefController {

    private final ChefService chefService;

    public ChefController(ChefService chefService) {
        this.chefService = chefService;
    }

    @GetMapping
    public List<Chef> getAllChefs(){
        return chefService.getChefs();
    }

}

```

And finally, we will add some initial rows to our database _(optional)_, for this we will create a data initializer class and implement the run method from the command runner interface.

```java
@Component
public class DataInitializer implements CommandLineRunner {

    private final ChefService chefService;

    public DataInitializer(ChefService chefService) {
        this.chefService = chefService;
    }

    @Override
    public void run(String... args) throws Exception {

        if(chefService.getNumberOfChefs() > 1) {
            System.out.println("Chefs already initialized!");
            return;
        }

        Chef chef1 = new Chef("Gordon Ramsay");
        Chef chef2 = new Chef("Jamie Oliver");
        Chef chef3 = new Chef("Anthony Bourdain");

        List<Recipe> chef1Recipes = Arrays.asList(
                new Recipe("Beef Wellington", "A classic British..."),
                new Recipe("Scrambled Eggs", "A simple breakfast..."),
                new Recipe("Beef Burger", "A juicy burger made...")
        );

        List<Recipe> chef2Recipes = Arrays.asList(
                new Recipe("Spaghetti Carbonara", "A creamy pasta...")
                new Recipe("Roast Chicken", "A classic roastchicken"),
                new Recipe("Fish and Chips", "A traditional...")
        );

        chefService.createChefWithRecipes(chef1,chef1Recipes);
        chefService.createChefWithRecipes(chef2,chef2Recipes);
        chefService.createChefWithRecipes(chef3,new ArrayList<>());

    }
}

```

### 3 - Package the application into a JAR file

Simply run the command `mvn package` and check that a jar file is generated under the folder `target`.

```bash
mvn package -DskipTests

```

Note that we have used `-DskipTests` option to skip tests because our application will attempt to connect to a database that does not exist yet.

![target file after packaging a spring boot application](https://cdn.hashnode.com/res/hashnode/image/upload/v1681745921058/bd54549b-60bb-4c38-89cb-5949021bf3d3.png)

The generated JAR file of our Spring Boot application is an executable archive that contains all the necessary components and dependencies required to run the application such as the compiled code, dependencies, embedded server and some additional resources...

We can simply run our application using the command `java -jar target/recipe-management-0.0.1-SNAPSHOT.jar`, this is exactly the command that we will need to run the application inside a container.

### 4 - Create the application's image (Dockerfile)

Here is the fun part!! But first, what is a docker file?

A `Dockerfile` is a text document that contains a set of instructions to build an image, these instructions could be extracting and copying files, or running commands...

Ok, let's create one in the root directory of our application, it should be named `Dockerfile` with a capital "D" _(Actually you can name it whatever you want, but to avoid headaches in the next steps let's respect the convention)_

```bash
FROM openjdk:17
VOLUME /tmp
EXPOSE 8080
COPY target/recipe-management-0.0.1-SNAPSHOT.jar recipe.jar
ENTRYPOINT ["java","-jar","/recipe.jar"]

```

- **FROM openjdk:17** : Every docker image needs a base image that contains the core operating system and other runtime components, and since we have a Spring Boot application we pulled the `OpenJDK:17 Java Runtime Environment (JRE)`.

- **VOLUME /tmp** : (optional) This instruction specifies that the `/tmp` directory in the docker container will be used as a volume to store temporary files and cache data... to share it between the docker container and the host filesystem or between the containers.

- **EXPOSE 8080** : This instruction informs Docker that the container will listen to port `8080` at runtime.

- **COPY target/recipe-management-0.0.1-SNAPSHOT.jar recipe.jar** : This instruction copies the `JAR` file to the working directory of the image which is by default `/`, the copied `JAR` file will be renamed to: `recipe.jar`_. (We can change the working directory using_ `WORKDIR` _instruction)._

- **ENTRYPOINT ["java","-jar","/recipe.jar"]**: For the first part `ENTRYPOINT` is used to configure the command that will be executed when a container is started, and as you may guess the second part specifies the actual command that runs our application.

Ok, we are ready to make some tests!! let's first build an image by running the command:

```bash
docker build . -t recipe:v1

```

The command above tells Docker to build the image from the file `Dockerfile` in the current directory. we can optionally use `-t` option to specify a name and a tag for our image.

![runinng docker build command](https://cdn.hashnode.com/res/hashnode/image/upload/v1681773383776/8419350a-f68e-42ec-9789-3b47b8e72e9f.png)

Now we have built our image successfully with the name and tag `recipe:v1`.

We can simply check that our image exists by running the command: `docker images`:

![list docker images](https://cdn.hashnode.com/res/hashnode/image/upload/v1681774699485/6dad4836-af51-4c05-9ec6-1a46c10d71e4.png)

### 5 - Creating containers

In this section, we will put everything together by creating a container of our app and linking it with a `PostgreSQL` database container since our app requires a database.

![spring boot and postgresql containers](https://cdn.hashnode.com/res/hashnode/image/upload/v1681811308715/da3478c9-07f0-4aa1-bd29-591f0c94a0b2.png)

We can achieve this in different ways but the most common and clear is using `Docker Compose`.

`Docker Compose` allows us to define and configure all of our application containers, link them together and specify dependencies between them in a single file: `docker-compose.yml`.

```yaml
version: "3.8"

services:
  psql-db:
    image: postgres
    container_name: psql-db
    restart: always
    environment:
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=1234
      - POSTGRES_DB=recipe
    ports:
      - '5432:5432'

  recipe:
    container_name: recipe_app
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - '8081:8080'
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://psql-db:5432/recipe
      - SPRING_DATASOURCE_USERNAME=admin
      - SPRING_DATASOURCE_PASSWORD=1234
      - SPRING_JPA_HIBERNATE_DDL_AUTO=update
    depends_on:
      - psql-db

```

1. First, we define the `yml` version.

2. Create the first container `psql-db` by using the Postgres official image `image: postgres`.

3. We give the container a name _(optional)_ `container_name: psql-db`_._

4. The `restart: always` parameter ensures that the container automatically will restart if it crashes.

5. The `environment` option could be different from one container to another for our Postgres database we need to have a database user, password, and name...

6. The `ports` option maps the container's internal port _(on the left side_ `5432:`_)_ to the host port `5432`. This allows other services running on the same host to connect to the Postgres database in the `psql-db` container using the host's IP address and port `5432`.

7. For the the `recipe` container we have made some small changes, we used `build` to tell Docker to build a new image from our `Dockerfile`.

8. We specified also in the environment section the database URL, USERNAME, and PASSWORD. notice that the database URL contains the name of the `Postgres` container and its port: `jdbc:postgresql://psql-db:5432/recipe`.

9. Finally, `depends_on` option tells the `recipe` container that it should not start until the `psql-db` container is running.

Now we are ready to create our containers using `docker-compose`.

_(If you are on a Linux machine, you will need to_ [_install Docker Compose_](https://docs.docker.com/compose/install/linux/)_)_

If your `docker-compose.yml` file in the root of your project just run:

```bash
docker-compose up 
# you can also use -d tag to start containers in the background.

```

![running docker-compose command](https://cdn.hashnode.com/res/hashnode/image/upload/v1681856600420/a891dac2-3a8f-4773-a525-c31edb330de6.png)

From the first few lines, it looks clear that docker has successfully built the recipe image based on the instructions from `Dockerfile`, it also created our `psql-db` and `recipe_app` containers and attached `psql-db` to `recipe_app` container.

Let's make an `http` request to make sure that everything is working as expected!

The endpoint is [http://localhost:8081/api/chefs](http://localhost:8081/api/chefs) and notice that we used the port `8081`.

![making http request using postman](https://cdn.hashnode.com/res/hashnode/image/upload/v1681857471279/ba0212ff-c5a3-4030-9aa9-f54dc05053f4.png)

Et Voila! the application is containerized! now we can share it with anyone have docker in his machine.