CodeGym /Courses /Docker for Spring /Readiness baseline for compose.yaml

Readiness baseline for compose.yaml

Docker for Spring
Level 17, Lesson 4
Available

1. Final readiness baseline

By this point, the problem has already been named: the short depends_on orders container startup, but does not wait until PostgreSQL really begins accepting connections. Now we will put together one neat, readable, and reproducible piece of compose.yaml that turns app + postgres from “starts sometimes” into “starts predictably.” This is not about making things more complex for the sake of complexity: it is about expressing the startup rule once so that Docker Compose, your future team, and you yourself two weeks from now will all understand it.

Readiness baseline in today's context is two connected settings. The first part lives with PostgreSQL and says: “this is how to check that I am truly ready.” The second part lives with the application and says: “I start only after the dependency has passed its health check.” Docker Compose states the problem directly: at startup it does not wait for the service to become “ready”; it only waits for the container to be “running”, and that is why a database needs healthcheck + depends_on.condition.

To make it completely concrete, here is a short “who is responsible for what” diagram:

Layer Where it lives What it guarantees
Database readiness signal postgres.healthcheck “PostgreSQL really is accepting connections”
Waiting for readiness app.depends_on.postgres.condition: service_healthy “The application does not start until the DB is healthy”
Connecting to the database SPRING_DATASOURCE_URL “The application goes to the DB by service name and container port”

2. The final compose.yaml, piece by piece

Now we will do what is usually called a “baseline”: a minimal but professional template. The idea is not to write the shortest YAML on the planet, but to make it read like a scenario. A good compose.yaml is like a clear contract: who starts, what they wait for, and how to tell that everything really works.

Let us start with the application part. We need two things: a long depends_on with service_healthy and Spring Boot configuration for postgres mode. The important thing is that we do not change the image and do not change the business code: only the startup environment changes, as it should in the container world.

Fragment 1 — app: wait for the DB and connect

services:
  app: # application service
    build: .
    depends_on:
      postgres:
        condition: service_healthy # start only when postgres becomes healthy
    environment:
      SPRING_PROFILES_ACTIVE: postgres # enable the profile with Postgres settings
      # Important: `postgres` is the service name in the Compose network, and `5432` is the container port
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog

The key point here is the line condition: service_healthy: Compose really can wait for a dependency's healthcheck to pass before starting the dependent service. And the second point is the connection URL: postgres:5432, that is, service name and container port. If you try to use localhost, you are back in the world of “why did it crash.” Inside a container, localhost means the container itself, not the neighboring database.

In a real project, there will almost certainly also be SPRING_DATASOURCE_USERNAME and SPRING_DATASOURCE_PASSWORD. I am intentionally not spelling them out in this first fragment so that a beginner's eye does not drown in details, but the logic is simple: these values must match POSTGRES_USER / POSTGRES_PASSWORD on the database service.

Fragment 2 — postgres: healthcheck with pg_isready

Now let us add what makes “ready” measurable: a healthcheck for PostgreSQL. In the Docker documentation on startup order, the approach is shown directly: the readiness of a dependency is expressed through healthcheck, and the dependent service waits for service_healthy.

services:
  postgres: # database service
    image: postgres:17 # Postgres image for the current setup
    environment:
      POSTGRES_DB: catalog # database name the container will create on first startup
      POSTGRES_USER: catalog # database user
      POSTGRES_PASSWORD: catalog # database user password
    healthcheck:
      # Readiness check command: "can it accept connections yet?"
      test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]

pg_isready is a tiny “doctor” that asks PostgreSQL: “are you ready to accept connections, or have you not had your coffee yet?” In the Docker Java guide, this is exactly how they recommend checking the database: healthcheck with pg_isready and waiting for service_healthy on the application side.

Now let us add timing parameters. They do not affect the “correctness” of the idea, but they do affect how well it behaves on different machines.

services:
  postgres:
    healthcheck:
      interval: 5s # how often we check readiness
      timeout: 3s # how long we wait for one check response
      retries: 10 # how many failed checks in a row are allowed
      start_period: 10s # "grace" time for warm-up before strict checks

These four numbers are not “magical”, but they should express the idea: “give the database a little time to start, then check regularly until it becomes healthy.”

Fragment 3 — full baseline

When everything is put together, it looks roughly like this. Yes, this is more than 10 lines, but this is one of those cases where compactness is worse than readability:

services:
  app: # application (Spring Boot)
    build: .
    depends_on:
      postgres:
        condition: service_healthy # wait until the DB becomes healthy
    environment:
      SPRING_PROFILES_ACTIVE: postgres
      # Connect by the service name in the Compose network, NOT by localhost
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog
      # These creds must match POSTGRES_USER/POSTGRES_PASSWORD on postgres
      SPRING_DATASOURCE_USERNAME: catalog
      SPRING_DATASOURCE_PASSWORD: catalog
    ports:
      - "8080:8080" # expose the application port on the host

  postgres: # database
    image: postgres:17
    environment:
      POSTGRES_DB: catalog
      POSTGRES_USER: catalog
      POSTGRES_PASSWORD: catalog
    healthcheck:
      # DB readiness: check whether Postgres accepts connections
      test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

If you are looking at YAML right now and thinking, “why could this not all be one button?”, then congratulations: you have just felt why DevOps engineers sometimes look tired. But the good news is that this is truly a small, clear, and standard baseline.

And yes: compose.yaml is the canonical file name, and it is considered the preferred path in the Compose model.

3. Real startup: events and signs

We often try to judge a system by one sign: “the container started.” In the case of a database, this is especially tricky: the container can be Up, the process is alive, but the DB itself is not yet ready for connections. That is why in the readiness model we deliberately look at the sequence: “postgres started → postgres healthy → app started.” This is not philosophy; it is a way to stop depending on luck and laptop speed.

Here is how this can be represented as a scenario:

sequenceDiagram
    participant C as docker compose
    participant P as postgres
    participant A as "app (Spring Boot)"

    C->>P: start container
    P-->>P: "initialization (starting up)"
    C->>P: "healthcheck: pg_isready (loop)"
    P-->>C: "healthy (exit code 0)"
    C->>A: "start container (depends_on: service_healthy)"
    A-->>P: connect jdbc:postgresql://postgres:5432/catalog
    A-->>C: started OK

If the diagram feels too “smart”, you can reduce it to one simple observation: the application should not crash on startup with Connection refused just because the database starts a couple of seconds later.

Good startup logic almost always reads in logs like a human story. Roughly like this:

postgres-1 | database system is ready to accept connections
app-1      | Started CatalogApplication in 4.123 seconds

This is not “decorative beauty”, but the main result of the day: the stack starts reliably because waiting is expressed through a checkable state, not through “let us just wait 20 seconds and hope.”

4. Diagnosing the readiness baseline

The strength of the readiness baseline is that it does not just “work” but is also “observable.” We do not start the stack blindly and guess from coffee grounds. We look at statuses and logs. And we do not need a full observability stack for this; two commands are enough, the ones you have already seen: docker compose ps and docker compose logs.

Here is the canonical minimal set. It is truly worth keeping in mind as “open the hood”:

docker compose ps # view container statuses and healthcheck (healthy/unhealthy)
docker compose logs -f app postgres # read the application and database logs at the same time

In docker compose ps, the column you care about is not only “Up”, but health. When a service has a healthcheck, the status changes and starts carrying meaning. In Docker documentation, this meaning is described literally: service_healthy means the dependency must be “healthy”, and “healthy” is defined by healthcheck.

An important psychological point: if you read only application logs, you will almost certainly start “fixing the wrong thing.” For example, you might change SPRING_DATASOURCE_URL, although the problem was not the URL but the database startup time. When you look at the app and postgres logs together, the cause-and-effect relationship becomes annoyingly obvious. And annoyingly obvious is, in fact, a compliment to infrastructure.

You can even keep a small table of signal interpretation:

What you see What it means What to do mentally
postgres ... Up (healthy) The DB has passed the check The application is allowed to start
postgres ... Up without (healthy) healthcheck is missing or not working You cannot use service_healthy as a guarantee
app ... Exited (1) The application crashed during startup Immediately check the app and postgres logs together
postgres ... (unhealthy) healthcheck is failing The problem is either in the check parameters or in the DB itself

5. healthcheck parameters

The numbers in healthcheck are not magical. For a local app + postgres setup, this set does exactly what we need: the check runs often enough, one slow attempt does not declare the database broken, and start_period gives it time to survive a cold start peacefully.

If PostgreSQL on your machine really wakes up more slowly, you should adjust these parameters, not add a fixed-delay to the application startup.

Baseline quality criterion

A good sign is that the stack can be brought up several times in a row, and app no longer fails with a random Connection refused at startup. After that, the problem shifts from timing to data lifecycle: now it becomes important not to “guess readiness” but to handle migrations, reset, and recovery of the local database properly.

6. Typical mistakes when configuring the readiness baseline

Mistake #1: the healthcheck was written “for show.”
Sometimes you see test: ["CMD", "true"] or something similar that always succeeds. This is worse than having no healthcheck at all, because it creates an illusion of reliability: Compose will start treating the service as “healthy” instantly, and the application will start and fail just as before. Readiness must measure PostgreSQL's actual ability to accept connections, and pg_isready is exactly such a practical indicator.

Mistake #2: depends_on was left in short form.
The short form depends_on: - postgres sets order, but not readiness. As a result, you think you “made a dependency”, but the race condition remains. You need the long form with condition: service_healthy, because Compose waits for healthcheck to pass only for dependencies marked as service_healthy.

Mistake #3: the application connects to localhost or to the host port.
This is a classic habit after running locally without Docker: “the DB is on localhost.” In the Compose network, localhost inside a container is the application container itself, not the database. Therefore the JDBC URL inside Compose must point to the service name postgres and the container port 5432. If you use the host port, you are effectively trying to bypass the Compose network and reintroduce unnecessary fragility.

Mistake #4: YAML indentation swallowed your condition, and you did not notice.
This is a quiet pain: Compose does not always “shout” in the way you want, and a person can easily miss one space. When condition is not under the right key, Compose simply treats depends_on as a normal list and does not wait for the healthcheck. If Connection refused suddenly appears again at startup, the first thing to check is not the Java code, but the YAML structure.

Mistake #5: overly aggressive timeout and start_period.
If you set start_period: 0s and timeout: 1s, you will get “unhealthy” even on a normal database, simply because it had not had time to start and the check is too nervous. In the Docker startup order example, they directly show that start_period is often made noticeable so that healthcheck is honest with respect to the database startup phase.

1
Task
Docker for Spring, level 17, lesson 4
Locked
Final readiness baseline for `compose.yaml`
Final readiness baseline for `compose.yaml`
1
Task
Docker for Spring, level 17, lesson 4
Locked
Startup Script and smoke-check without manual pauses
Startup Script and smoke-check without manual pauses
1
Survey/quiz
Docker Ready, level 17, lesson 4
Unavailable
Docker Ready
Starting and checking services
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION