1. Introduction

For this tutorial, we want to run a Spring Boot application with the popular open-source database PostgreSQL. In a previous article, we looked at Docker Compose to handle multiple containers at once. So instead of installing PostgreSQL as a separate application, we’ll use Docker Compose to run Spring Boot and PostgreSQL.

2. Creating the Spring Boot Project

Let’s go to the Spring Initializer and create our Spring Boot project. We’ll add the PostgreSQL Driver and Spring Data JPA modules. After we download the resulting ZIP file and extract it to a folder, we can run our new application:

./mvnw spring-boot:run

The application fails because it can’t connect to the database:

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

3. Dockerfile

Before we can start PostgreSQL with Docker Compose, we need to turn our Spring Boot application into a Docker image. The first step is to package the application as a JAR file:

./mvnw clean package -DskipTests

Here, we’ll first clean-up our previous builds before packaging the application. In addition, we’ll skip the tests because they fail without PostgreSQL.

We now have an application JAR file in the target directory. That file has the project name and version number in its name and ends with -SNAPSHOT.jar. So its name could be docker-spring-boot-postgres-0.0.1-SNAPSHOT.jar.

Let’s make the new src/main/docker directory. After that, we’ll copy the application JAR file there:

cp target/docker-spring-boot-postgres-0.0.1-SNAPSHOT.jar src/main/docker

Finally, we’ll create this Dockerfile in that same directory:

FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} application.jar
ENTRYPOINT ["java", "-jar", "application.jar"]

This file describes how Docker should run our Spring Boot application. It uses Java 11 from AdoptOpenJDK, and copies the application JAR file to application.jar. It then runs that JAR file to start our Spring Boot application.

4. Docker Compose File

Now let’s write our Docker Compose file, docker-compose.yml, and save it in src/main/docker:

version: '2'

services:
  app:
    image: 'docker-spring-boot-postgres:latest'
    build:
      context: .
    container_name: app
    depends_on:
      - db
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/compose-postgres
      - SPRING_DATASOURCE_USERNAME=compose-postgres
      - SPRING_DATASOURCE_PASSWORD=compose-postgres
      - SPRING_JPA_HIBERNATE_DDL_AUTO=update
          
  db:
    image: 'postgres:13.1-alpine'
    container_name: db
    environment:
      - POSTGRES_USER=compose-postgres
      - POSTGRES_PASSWORD=compose-postgres

Our application’s name is app. It’s the first of two services (lines 4-15):

  • The Spring Boot Docker image has the name docker-spring-boot-postgres:latest (line 5). Docker builds that image from the Dockerfile in the current directory (lines 6-7).
  • The container name is app (line 8). It depends on the db service (line 10). That’s why it starts after the db container.
  • Our application uses the db PostgreSQL container as the data source (line 12). The database name, the user name, and the password are all compose-postgres (lines 12-14).
  • Hibernate will automatically create or update any database tables needed (line 15).

The PostgreSQL database has the name db and is the second service (lines 17-22):

  • We use PostgreSQL 13.1 (line 18).
  • The container name is db (line 19).
  • The user name and password are both compose-postgres (lines 21-22).

5. Running With Docker Compose

Let’s run our Spring Boot application and PostgreSQL with Docker Compose:

docker-compose up

First, this will build the Docker Image for our Spring Boot application. Next, it will start a PostgreSQL container. Finally, it will launch our application Docker image. This time, our application runs fine:

Starting DemoApplication v0.0.1-SNAPSHOT using Java 11.0.9 on f94e79a2c9fc with PID 1 (/application.jar started by root in /)
[...]
Finished Spring Data repository scanning in 28 ms. Found 0 JPA repository interfaces.
[...]
Started DemoApplication in 4.751 seconds (JVM running for 6.512)

As we can see, Spring Data found no repository interface. That is correct, as we didn’t create one yet.

If we want to stop all containers, we need to press [Ctrl-C] first. Then we can stop Docker Compose:

docker-compose down

6. Creating a Customer Entity and Repository

To use the PostgreSQL database in our application, we’ll create a simple customer entity:

@Entity
@Table(name = "customer")
public class Customer {

    @Id
    @GeneratedValue
    private long id;
    
    @Column(name = "first_name", nullable = false)
    private String firstName;
    
    @Column(name = "last_name", nullable = false)
    private String lastName;

The Customer has a generated id attribute and two mandatory attributes: firstName and lastName.

Now we can write the repository interface for this entity:

public interface CustomerRepository extends JpaRepository<Customer, Long> { }

By simply extending JpaRepository, we’ll inherit methods for creating and querying our Customer entity.

Finally, we’ll use these methods in our application:

@SpringBootApplication
public class DemoApplication {
    @Autowired 
    private CustomerRepository repository; 
  
    @EventListener(ApplicationReadyEvent.class)
    public void runAfterStartup() {
        List allCustomers = this.repository.findAll(); 
        logger.info("Number of customers: " + allCustomers.size());
 
        Customer newCustomer = new Customer(); 
        newCustomer.setFirstName("John"); 
        newCustomer.setLastName("Doe"); 
        logger.info("Saving new customer..."); 
        this.repository.save(newCustomer); 
 
        allCustomers = this.repository.findAll(); 
        logger.info("Number of customers: " + allCustomers.size());
    }
}
  • We access our Customer repository through dependency injection.
  • We query the number of existing customers with the repository; this will be zero.
  • Then we create and save a customer.
  • When we then query the existing customers again, we’ll expect to find the one we just created.

7. Running With Docker Compose Again

To run the updated Spring Boot application, we need to rebuild it first. Therefore, we execute these commands once more in the project root directory:

./mvnw clean package -DskipTests
cp target/docker-spring-boot-postgres-0.0.1-SNAPSHOT.jar src/main/docker

How do we rebuild our Docker image with this updated application JAR file? The best way is to remove the existing Docker image whose name we specified in the docker-compose.yml. This forces Docker to build the image again the next time we start our Docker Compose file:

cd src/main/docker
docker-compose down
docker rmi docker-spring-boot-postgres:latest
docker-compose up

So after stopping our containers, we’ll delete the application Docker image. We’ll then start our Docker Compose file again, which rebuilds the application image.

Here’s the application output:

Finished Spring Data repository scanning in 180 ms. Found 1 JPA repository interfaces.
[...]
Number of customers: 0
Saving new customer...
Number of customers: 1

Spring Boot finds our empty customer repository. Therefore, we start with no customer, but then successfully create one.

8. Conclusion

In this brief article, we started by creating a Spring Boot application for PostgreSQL. Next, we wrote a Docker Compose file to run our application container with a PostgreSQL container.

Finally, we created a customer entity and repository, which allowed us to save a customer to PostgreSQL.

As usual, the source code can be found over on GitHub.