Docker Compose and Spring Boot: Let the Application Start the Database
Two different jobs hide in this question. To develop against a database, add the Docker Compose Support dependency: Spring Boot starts the container, waits for it to report healthy, and injects the connection details — verified in a project whose application.properties is 31 bytes and has no JDBC URL. To run the whole stack, write both services yourself, and know that depends_on does not wait for readiness.
Search this and you get a compose.yaml with two services and a depends_on. That file is correct, and it answers a question you may not be asking.
There are two jobs hiding in this query. Running the whole stack — application and database together, for a demo or for CI. And developing against a dependency — you want a Postgres, you do not want to install one, and you are going to change the application code fifty times this afternoon. The two-service file is right for the first job and it is why people end up rebuilding an image every time they change a line.
Spring Boot has shipped the answer to the second job since 3.1, and most writing on this query predates it.
Let Spring Boot drive compose
Tick Docker Compose Support in Spring Initializr and two things arrive. A dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
And a compose file, generated for you:
services:
postgres:
image: 'postgres:latest'
environment:
- 'POSTGRES_DB=mydatabase'
- 'POSTGRES_PASSWORD=secret'
- 'POSTGRES_USER=myuser'
ports:
- '5432'
Note that ports entry: a container port with no host port, so Docker assigns one. You are not expected to know which — that is the point of what happens next.
Now ./mvnw spring-boot:run, and here is the log:
DockerComposeLifecycleManager : Using Docker Compose file .../compose.yaml
DockerCli : Image postgres:latest Pulling
DockerCli : Container app-postgres-1 Created
DockerCli : Container app-postgres-1 Started
DockerCli : Container app-postgres-1 Healthy
HikariDataSource : HikariPool-1 - Starting...
HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection...
HikariDataSource : HikariPool-1 - Start completed.
Read the order. It pulled the image, created the container, started it, waited for it to report healthy, and only then connected the pool.
And the connection details came from nowhere you wrote. The whole of application.properties:
spring.application.name=orders
Thirty-one bytes. Grepping the entire source tree for datasource or jdbc returns nothing. Spring Boot inspected the running compose service, worked out the assigned port, and configured the DataSource from it.
When the application stopped, so did the container:
app-postgres-1 Exited (0)
Tip
That last behaviour is the property to know:
spring.docker.compose.lifecycle-managementtakesstart-and-stop(the default),start-only— start it and leave it running, so the next run does not pay the startup cost — andnone, for when something else owns the containers.noneis what you want in CI.
The compose file for running everything
The other job. Both services, the application built from a Dockerfile:
services:
postgres:
image: 'postgres:16'
environment:
POSTGRES_DB: orders
POSTGRES_USER: orders
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U orders"]
interval: 2s
retries: 15
app:
build: .
ports:
- '8080:8080'
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/orders
SPRING_DATASOURCE_USERNAME: orders
SPRING_DATASOURCE_PASSWORD: secret
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:
Three things in there are worth pointing at.
postgres:5432, not localhost. A service name is a hostname on the compose network. localhost inside the app container is the app container.
The datasource URL is written here, because the application is no longer inspecting compose from outside — it is a service inside it. This is the version where you configure connection details, and environment variables are the right place.
condition: service_healthy — which is the next section.
depends_on does not mean ready
This is the single most common failure in a hand-written compose file, and Docker's own documentation is explicit: depends_on controls startup order and does not wait for a dependency to be ready.
A Postgres container is "started" almost immediately and accepts connections a few seconds later. An application that starts in between gets a connection refused and, depending on your configuration, either retries or dies.
So a bare depends_on: [postgres] buys you almost nothing. The fix is two parts, both shown above: a healthcheck on the database that actually tests it, and condition: service_healthy on the thing that depends on it.
Which is exactly what Spring Boot's own integration did without being asked — the Container app-postgres-1 Healthy line in the first log is it waiting. A hand-written file has to be told.
Common mistake
Adding
depends_onand concluding the ordering problem is solved. It usually appears to work on a warm machine where the image is cached and Postgres starts fast, and fails on CI where everything is cold.
Two things people lose
Data, to a missing volume. A container's filesystem goes with the container. The pgdata volume above is what makes the database survive a docker compose down; without it every restart is an empty database. In development that is often what you want — and never what you want to discover by accident.
Secrets, to a committed file. POSTGRES_PASSWORD: secret is fine in a compose file for local development and is not a pattern to carry anywhere else. That file is a development tool, not a deployment descriptor, and a real deployment gets its credentials from somewhere else entirely.
And one alternative worth knowing: for tests, Testcontainers is usually the better tool, because it starts containers from the test's own lifecycle rather than depending on something you started by hand. Compose owns the development loop and the whole-stack run; Testcontainers owns the test run. They are not competing.
The image the app service builds is a separate decision with its own trade-offs, and where the whole thing eventually runs — one host or a scheduler — is a different question again. If you are starting from an empty project, create one with Docker Compose Support and PostgreSQL ticked, and the first half of this article is already done for you. Which database goes in the file is yours to choose.
Frequently asked questions
- Does Spring Boot really start Docker Compose for me?
- Yes, from 3.1 onward, with the spring-boot-docker-compose dependency. The captured log shows it: it found compose.yaml, pulled postgres:latest, created and started the container, waited for it to report Healthy, and only then connected the pool. The project had no datasource properties at all.
- Why does my app fail to connect even though I used depends_on?
- Because depends_on controls startup order and does not wait for the dependency to be ready — Docker's own documentation says so. A database container is "started" long before it accepts connections. Add a healthcheck to the database service and condition: service_healthy to the dependent one.
- Should the application itself be a compose service?
- It depends which job you are doing. For running the whole stack — a demo, CI, a colleague's machine — yes. For your own inner loop, no: every code change then means rebuilding the application image before you can test it. Put only the dependencies in compose and run the app from your IDE.
- Where does my data go when the container stops?
- Nowhere, unless you declared a volume. A container's filesystem disappears with it, so a Postgres service with no volume starts empty every time. That is often what you want in development and never what you want by accident.
- Is Testcontainers better than Docker Compose for this?
- For tests, usually yes — it starts containers from the test's own lifecycle, so a test run cannot depend on something you started by hand yesterday. Compose is better for the development loop and for running a whole stack. They are not competing for the same job.