Showing posts with label Docker Compose. Show all posts
Showing posts with label Docker Compose. Show all posts

Monday, January 15, 2024

Spring Boot - Docker Compose - Run init script . . .

Spring Boot 3.1 enhanced docker-compose support, made it lot simpler and better suited for local development. With that we don't need to worry about installing services like database locally and managing them manually, letting Docker do that for us and Spring Boot do the rest of starting and stopping docker container.
 
This post is about details explored on - how to run additional init db script with PostgreSQL service defined in Docker compose file in a Spring Boot application.

Environment: Java 21, Spring Boot 3.2.1, PostgreSQL 16, maven 3.9.6 on macOS Catalina 10.15.7

The Scenario

My Spring Boot 3.2.x application uses PostgreSQL database, a specific version of it. By leveraging Spring Boot support for docker-compose in development, I would like to have a new schema and user  created, granting the user required privileges on the schema.

A typical PostgreSQL Service configuration in docker compose file looks like:
docker-compose.yml
version: '3' services: PostgreSQL16: image: 'postgres:16.1' ports: - '54321:5432' environment: - 'POSTGRES_DB=my_app' - 'POSTGRES_USER=postgres' - 'POSTGRES_PASSWORD=s3cr3t'

In the above docker compose configuration, we have specified database name, user, and password through environment variables in the container, and mapped host port (local port) to container port (default postgres port). With this when the docker-compose command: docker-compose up is run to create and start the container, the my_app database gets created, and PostgreSQL will be up and running in the container. The postgres user created is the superuser with access and ownership to all database objects including the public schema. When the docker container is created for PostgreSQL16 database service, the value of POSTGRES_USER environment variable is used to create the superuser, and public is the default schema created.

PostgreSQL 15 changes to public schema

From version 15 onwards, privileges on public schema are restricted and the schema is accessible to superuser only. So, it is good to create an application specific schema, and application specific database user with all needed privileges granted on the application schema. This requires a way to run a one-time initial databases script for creating application schema and user. The following shell script is an example to do so:
init-database.sh
#!/bin/sh set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL /* Create schema, user and grant permissions */ CREATE SCHEMA my_app_schema; CREATE USER my_app_user_local WITH PASSWORD 'password'; GRANT ALL PRIVILEGES ON SCHEMA my_app_schema TO my_app_user_local; EOSQL

In order to run the above db init script when the container is created, reference the shell script file under volumes: to attach the init file directory (./) to the container directory (/docker-entrypoint-initdb.d/) as shown below:
docker-compose.yml
version: '3' services: PostgreSQL16: image: 'postgres:16.1' ports: - '54321:5432' environment: - 'POSTGRES_DB=my_app' - 'POSTGRES_USER=postgres' - 'POSTGRES_PASSWORD=s3cr3t' volumes: -  ./init-database.sh:/docker-entrypoint-initdb.d/init-database.sh
 
With this, when the docker container is created for PostgreSQL service, the init db script gets executed which results with new schema my_app_schema and user my_app_user_local. with privileges granted.

Gotchas

Auto configured datasource properties
When the application is run, the PostgreSQL container is created and run by Spring Boot. It also auto configures dataSource bean with properties: url, username, and password taking them from docker compose file. The user is superuser created from the POSTGRES_USER container environment variable. If the application has any initialization database scripts within the application under main/resources dir like schema.sql for initial schema or even flyway scripts in flyway enabled application under main/resources/db.migration dir, all the database tables and other objects created are owned by the superuser as the datasource uses superuser for connecting to the database.

If you want the data objects like tables, indices created in the application schema instead of public, you may need to specify it as the prefix in your schema.sql or for an app with flyway support, add the property spring.flyway.schemas appropriately, e.g in application.yml.

TIPS

1. With docker-compose managing the database service, if you need to use psql, the terminal based frontend to PostgreSQL to connect to db and run commands, invoke psql like:

$ # list docker containers running $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7caf031c31a4 postgres:16.0 "docker-entrypoint.s…" 56 minutes ago Up 56 minutes 0.0.0.0:5222->5432/tcp docker-PostgreSQL16-1 45fa1a477ac3 postgres:15.3 "docker-entrypoint.s…" 4 days ago Up 3 days 0.0.0.0:54321->5432/tcp docker-compose-postgres15-1 $ # run psql command to coonect to PostgreSQL16 db and list users $ docker exec -it docker-PostgreSQL16-1 psql -U postgres psql (16.0 (Debian 16.0-1.pgdg120+1)) Type "help" for help. postgres=# postgres=# \? General \bind [PARAM]... set query parameters \copyright show PostgreSQL usage and distribution terms \crosstabview [COLUMNS] execute query and display result in crosstab \errverbose show most recent error message at maximum verbosity \g [(OPTIONS)] [FILE] execute query (and send result to file or |pipe); \g with no arguments is equivalent to a semicolon \gdesc describe result of query, without executing it \gexec execute query, then execute each value in its result \gset [PREFIX] execute query and store result in psql variables \gx [(OPTIONS)] [FILE] as \g, but forces expanded output mode \q quit psql --More-- postgres=# \dn List of schemas Name | Owner --------+------------------- public | pg_database_owner (1 row) postgres=# \du List of roles Role name | Attributes -------------------+------------------------------------------------------------ my_app_user_local | postgres | Superuser, Create role, Create DB, Replication, Bypass RLS postgres=# SELECT version(); version --------------------------------------------------------------------------------------------------------------------- PostgreSQL 16.0 (Debian 16.0-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit (1 row) postgres=# \conninfo You are connected to database "postgres" as user "postgres" via socket in "/var/run/postgresql" at port "5432". postgres=# select current_date; current_date -------------- 2024-01-15 (1 row) postgres=# SHOW search_path; search_path ----------------- "$user", public (1 row) postgres=# \l List of databases Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges --------------+----------+----------+-----------------+------------+------------+------------+-----------+----------------------- boot-graalvm | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | postgres | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | template0 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres (4 rows) postgres=# \c boot-graalvm You are now connected to database "boot-graalvm" as user "postgres". boot-graalvm=# \dt List of relations Schema | Name | Type | Owner --------+-----------------------+-------+---------- public | account_holder | table | postgres public | accounts | table | postgres public | addresses | table | postgres public | flyway_schema_history | table | postgres (4 rows) boot-graalvm=# boot-graalvm=# \c postgres You are now connected to database "postgres" as user "postgres". postgres=# postgres=# \q

References

Friday, December 29, 2023

Testable Spring Boot Application main method with Docker Compose added - passing required arguments . . .

Java application's entry point is its main method which takes String array as argument. The command line arguments are passed by Java runtime system to the main method as an array of Strings.

Spring Boot application is a command line runnable Java application with a main method. The SpringApplication class is used to bootstrap and launch Spring app from Java's main method. Spring framework promotes POJO development and hence makes it easy to write testable code. A simple JUnit test can be written to test the main method.

This post is about writing a simple JUnit test case for Spring Boot application's main method in the context of Docker Compose dependency, and passing required arguments.

Environment: Java 21, Spring Boot 3.2.1, maven 3.9.6 on macOS Catalina 10.15.7

A typical, simple Spring Boot application class with a main method looks like:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GraalVmApplication { public static void main(String[] args) { SpringApplication.run(GraalVmApplication.class, args); } }

A simple JUnit test case to test the application looks like:
import org.junit.jupiter.api.Test; public class ApplicationStartsTest { @Test void application_starts() { GraalVmApplication.main(new String[]{}); } }

The test case is simple. It just invokes application's main method which in turn launches the Spring boot application and exits. Note that there are no arguments passed, so it's launched with default profile, which is ok to make sure the application runs. 

The Scenario

Now, let's says that we want to add JPA and Postgres DB support to our application. Also want to try Spring support for Docker Compose by adding Postgres database service docker container for database. Spring docker support takes care of starting up the Postgres docker container and bringing it down when the app exists. All it requires is to to add spring-boot-docker-compose dependency and compose.yaml or docker-compose.yaml under the root project. Refer to this Sample Git project.

With the JPA, spring-boot-docker-compose dependencies and docker-compose.yaml added, the above JUnit test case fails to run with the following exception:
*************************** 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 Action: Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active). [ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 3.731 s <<< FAILURE! -- in com.giri.boot.graalvm.ApplicationStartsTest [ERROR] com.giri.boot.graalvm.ApplicationStartsTest.applicationStarts -- Time elapsed: 3.705 s <<< ERROR! org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]: Unsatisfied dependency expressed through method 'dataSourceScriptDatabaseInitializer' parameter 0: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:802) at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:546) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1164) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:312) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1232) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:950) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:464) at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1358) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1347) at com.giri.boot.graalvm.GraalVmApplication.main(GraalVmApplication.java:12) at com.giri.boot.graalvm.ApplicationStartsTest.applicationStarts(ApplicationStartsTest.java:17) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:655) at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:643) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1164) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:911) at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:789)

The reason for the failure is -  by default docker compose support is disabled when running tests.

The Fix

The fix is to pass additional arguments like - enable docker compose support for this test, profile (local) for which datasource properties are configured etc. With these, when the unit test is run, docker compose is up, which brings up Postgres container and the application connects to the database through datasource configuration properties specified in application-local.yml file for local profile. With the fix, the test case looks like:

public class ApplicationStartsTest { @Test void applicationStarts() { GraalVmApplication.main( new String[]{ "--spring.profiles.active=local", "--spring.docker.compose.skip.in-tests=false", "--spring.docker.compose.file=docker/docker-compose.yaml" } ); } }

Now, the test case passes. Below is the output with docker compose highlighted:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.giri.boot.graalvm.ApplicationStartsTest arg: --spring.profiles.active=local arg: --spring.docker.compose.skip.in-tests=false arg: --spring.docker.compose.file=docker/docker-compose.yaml ____ _ ____ ___ __ | __ ) ___ ___ | |_ / ___|_ __ __ _ __ _| \ \ / / __ ___ | _ \ / _ \ / _ \| __| | | _| '__/ _` |/ _` | |\ \ / / '_ ` _ \ | |_) | (_) | (_) | |_ | |_| | | | (_| | (_| | | \ V /| | | | | | |____/ \___/ \___/ \__| \____|_| \__,_|\__,_|_| \_/ |_| |_| |_| :: Spring Boot :: 3.2.1 :: Running on Java :: 21 :: Application :: 0.0.1-SNAPSHOT 2023-12-28T20:09:11.059-05:00 INFO 65561 --- [boot-graalvm] [ main] c.giri.boot.graalvm.GraalVmApplication : Starting GraalVmApplication using Java 21 with PID 65561 (started by pottepalemg in /Users/pottepalemg/dev/boot-graalvm) 2023-12-28T20:09:11.065-05:00 INFO 65561 --- [boot-graalvm] [ main] c.giri.boot.graalvm.GraalVmApplication : The following 1 profile is active: "local" 2023-12-28T20:09:11.523-05:00 INFO 65561 --- [boot-graalvm] [ main] .s.b.d.c.l.DockerComposeLifecycleManager : Using Docker Compose file '/Users/pottepalemg/dev/boot-graalvm/docker/docker-compose.yaml' 2023-12-28T20:09:12.547-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Creating 2023-12-28T20:09:12.614-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Created 2023-12-28T20:09:12.618-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Starting 2023-12-28T20:09:13.169-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Started 2023-12-28T20:09:13.174-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Waiting 2023-12-28T20:09:13.682-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Healthy 2023-12-28T20:09:16.211-05:00 INFO 65561 --- [boot-graalvm] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2023-12-28T20:09:16.237-05:00 INFO 65561 --- [boot-graalvm] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 17 ms. Found 0 JPA repository interfaces. 2023-12-28T20:09:17.133-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2023-12-28T20:09:17.157-05:00 INFO 65561 --- [boot-graalvm] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-12-28T20:09:17.157-05:00 INFO 65561 --- [boot-graalvm] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.17] 2023-12-28T20:09:17.233-05:00 INFO 65561 --- [boot-graalvm] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-12-28T20:09:17.234-05:00 INFO 65561 --- [boot-graalvm] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2082 ms 2023-12-28T20:09:17.648-05:00 INFO 65561 --- [boot-graalvm] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2023-12-28T20:09:17.895-05:00 INFO 65561 --- [boot-graalvm] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@24a99b1c 2023-12-28T20:09:17.897-05:00 INFO 65561 --- [boot-graalvm] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2023-12-28T20:09:17.970-05:00 INFO 65561 --- [boot-graalvm] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2023-12-28T20:09:18.102-05:00 INFO 65561 --- [boot-graalvm] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.1.Final 2023-12-28T20:09:18.163-05:00 INFO 65561 --- [boot-graalvm] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2023-12-28T20:09:18.557-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2023-12-28T20:09:19.123-05:00 INFO 65561 --- [boot-graalvm] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) 2023-12-28T20:09:19.129-05:00 INFO 65561 --- [boot-graalvm] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2023-12-28T20:09:19.398-05:00 WARN 65561 --- [boot-graalvm] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning 2023-12-28T20:09:20.474-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 13 endpoint(s) beneath base path '/actuator' 2023-12-28T20:09:20.559-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' 2023-12-28T20:09:20.576-05:00 INFO 65561 --- [boot-graalvm] [ main] c.giri.boot.graalvm.GraalVmApplication : Started GraalVmApplication in 10.156 seconds (process running for 11.266) [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.57 s -- in com.giri.boot.graalvm.ApplicationStartsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------

💡 TIPS

It is also helpful to print arguments in the Spring boot application:
@SpringBootApplication public class GraalVmApplication { public static void main(String[] args) { for (String arg: args) { // let's examine arguments passed System.out.println("arg: " + arg); } SpringApplication.run(GraalVmApplication.class, args); } }

Conclusion

Much of the learning comes, not from working code, but from failing code. If everything works on the first go, there isn't much learned ;)
Keep exploring!
Happy Holidays!!

References

Thursday, December 28, 2023

Spring Boot, Docker compose on Mac OS Catalina - Gotcha . . .

Spring Boot 3.0 enhanced docker compose support with which container startup and destroy is taken care by spring boot and is quite convenient.

Anything I try first time, I hit road blocks. It's both good and bad. The good part is - it makes me explore things little deeper, which otherwise is not possible. The bad part is - it is frustrating and needs patience to get it to work.

Environment: Java 21, Spring Boot 3.2.0, maven 3.9.6 on macOS Catalina 10.15.7

Scenario

I wanted to add Postgres db service through docker compose. So, just added docker compose support to an existing simple Spring Boot 3.2.0 application. All that I had to do was simple. Add the dependency in pom.xml and a docker-compose.yml file in the root directory of the project. This is customizable through the property: spring.docker.compose.file.

pom.xml
... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-docker-compose</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> ...

docker-compose.yaml
services: postgres: image: 'postgres16:16.0' environment: - 'POSTGRES_DB=boot-graalvm' - 'POSTGRES_USER=postgres' - 'POSTGRES_PASSWORD=s3cr3t' ports: - '5222:5432'

Application fails to run with the following error:
2023-12-27T08:58:48.534-05:00 ERROR 229 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.boot.docker.compose.core.ProcessExitException: 'docker-compose version --format json' failed with exit code 1. Stdout: Stderr: Show version information and quit. Usage: version [--short] Options: --short Shows only Compose's version number. at org.springframework.boot.docker.compose.core.ProcessRunner.run(ProcessRunner.java:96) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.core.ProcessRunner.run(ProcessRunner.java:74) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.core.DockerCli$DockerCommands.getDockerComposeCommand(DockerCli.java:165) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.core.DockerCli$DockerCommands.(DockerCli.java:130) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.core.DockerCli.lambda$new$0(DockerCli.java:65) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1228) ~[na:na] at org.springframework.boot.docker.compose.core.DockerCli.(DockerCli.java:64) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.core.DockerCompose.get(DockerCompose.java:92) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.lifecycle.DockerComposeLifecycleManager.getDockerCompose(DockerComposeLifecycleManager.java:149) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.lifecycle.DockerComposeLifecycleManager.start(DockerComposeLifecycleManager.java:110) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.lifecycle.DockerComposeListener.onApplicationEvent(DockerComposeListener.java:53) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.boot.docker.compose.lifecycle.DockerComposeListener.onApplicationEvent(DockerComposeListener.java:35) ~[spring-boot-docker-compose-3.2.0.jar:3.2.0] at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:178) ~[spring-context-6.1.1.jar:6.1.1] at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:171) ~[spring-context-6.1.1.jar:6.1.1] at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:149) ~[spring-context-6.1.1.jar:6.1.1] at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:137) ~[spring-context-6.1.1.jar:6.1.1] at org.springframework.boot.context.event.EventPublishingRunListener.multicastInitialEvent(EventPublishingRunListener.java:136) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.context.event.EventPublishingRunListener.contextLoaded(EventPublishingRunListener.java:98) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.SpringApplicationRunListeners.lambda$contextLoaded$4(SpringApplicationRunListeners.java:72) ~[spring-boot-3.2.0.jar:3.2.0] at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na] at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:118) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:112) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.SpringApplicationRunListeners.contextLoaded(SpringApplicationRunListeners.java:72) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.SpringApplication.prepareContext(SpringApplication.java:431) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.SpringApplication.run(SpringApplication.java:322) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1342) ~[spring-boot-3.2.0.jar:3.2.0] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1331) ~[spring-boot-3.2.0.jar:3.2.0] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------


The Fix

The issue was the older version of Docker Desktop 2.5.0.1 running on my laptop. The menu item Check for Updates... wouldn't even work. It seemed like I was stuck with very old version of Docker Desktop. The fix was to upgrade to a version that works. Yes, you have to find out the version that works for Mac OS Catalina. I tried and learned the fact that Docker Desktop latest version (4.26.1) doesn't run on Mac OS Catalina and Docker Desktop silently removed the support for Mac OS Catalina in 4.16.0. So the only version to upgrade for Mac OS Catalina is: 4.15.0.

Jeez, what a mess of higher elevated software development. It's just ridiculous!

There is no easy way even to find the version 4.15.0 download link. Finally found the links for both the versions: 2.5.0.1 that I had and 4.15.0 that I want to upgrade to. 

To downgrade to 2.5.0.1, here is the link: https://desktop.docker.com/mac/stable/49550/Docker.dmg
To upgrade to 4.15.0, here is the link: https://desktop.docker.com/mac/main/amd64/93002/Docker.dmg

TIPS

1. Spring Boot docker compose out-of-the-box looks for docker compose file named compose.yaml or docker-compose.yaml in the project root directory. If you want to place the file anywhere else under the project root folder, for instance like a docker sub-dir under project root, here is how the customization property should look like in the appropriate application.yml or application-<env>.yml related yml file:

spring: docker: compose: file: docker/docker-compose.yaml

2. Docker compose is enabled by default. When the dependent jar is found, it looks for compose.yaml/docker-compose.yaml. It can be disabled by setting explicitly the property spring.docker.enabled to false. Typically, docker compose is good for local development. If there are multiple profiles (environments) that the application is configured to run in, the application.properties or application.yml file should have docker compose disabled and it can be enabled in the appropriate profile that it's used, e.g local.

3. In the compose.yaml or docker-compose.yaml file, note that we specify the HOST_POST and CONTAINER_PORT for PostgreSQL in the format HOST_PORT:CONTAINER_PORT. The container port is 5432 (postgres default). The host port also can be 5432. But in case if you want to have multiple postgres databases managed by Docker for multiple applications/versions, you need to use different HOST_PORT for each image for both to be up and running at the same time. The HOST_PORT is the port you use to connect to the database.

4. Give a unique name for each postgres image if you need to have multiple versions of postgres instances managed by Docker. Otherwise, you will end up having just one. You can use version number in the image name to distinguish.

5. Check docker and docker-compose versions:
$ docker -v or docker --version
$ docker compose version

6. If the application fails with the following exception:

org.springframework.boot.docker.compose.lifecycle.ReadinessTimeoutException: Readiness timeout of PT2M reached while waiting for services

Add the following to docker-compose.yaml file 
services: postgres: image: 'postgres16:16.0' environment: - 'POSTGRES_DB=boot-graalvm' - 'POSTGRES_USER=postgres' - 'POSTGRES_PASSWORD=s3cr3t' ports: - '5222:5432' labels: org.springframework.boot.readiness-check.tcp.disable: true

References