CodeGym /Kursy /Docker for Spring /depends_on: startup and PG readiness

depends_on: startup and PG readiness

Docker for Spring
Poziom 17, Lekcja 0
Dostępny

1. The problem: depends_on does not save you

We already have a working app + postgres setup: the application connects to the database by the service name postgres, the postgres profile is enabled through env vars, and compose.yaml looks quite plausible. The next annoyance does not appear in the config, but in startup timing: the stack comes up, but sometimes app still fails in the first few seconds with a database connection error.

At this stage, a beginner Docker/Compose user usually has a sincere (and fair) grievance with reality: you added depends_on, Compose really does start PostgreSQL before the application, but sometimes app still fails on startup with a database connection error. It feels like “random magic”, but in fact it is a clean engineering difference between startup order and service readiness.

Let us start with what depends_on usually looks like in the first approximation — in its short form. It is very pleasant: easy to read, looks like a “proper dependency”, and instantly creates the expectation, “so Compose will wait for the database”.

services:
  app:
    # Short form: controls container startup order
    depends_on:
      - postgres

  postgres:
    # The rest of the service fields are not important right now; the key point is that postgres starts before app
    image: postgres:17

At the level of startup order, this really works: Compose first creates and starts the postgres container, then the app container. But the key point is that the short form of depends_on says nothing about the moment when PostgreSQL becomes ready to accept connections. It does not promise to “wait until the database warms up”, “wait until initialization finishes”, or “wait until all internal processes are up”. It only says: “Start the containers in this order.”

And here it helps to slightly change your mindset: Compose is not a “readiness manager for applications”, but rather a “container startup organizer”. It does not read your thoughts and does not know what “PostgreSQL is ready” means in the terms of your application. For Spring Boot in the postgres profile, that means one concrete thing: the application must be able to open a JDBC connection, and often also complete the first steps of persistence-layer startup. But Compose, in the short form of depends_on, does not know about that.

To make sure this does not feel like “philosophy for philosophy’s sake”, let us add some context from our project. In compose.yaml, app usually already has env vars, and everything looks absolutely correct. The host is correct (postgres), the port is correct (5432), the profile is correct.

services:
  app:
    environment:
      # Spring Boot profile that enables the PostgreSQL connection
      SPRING_PROFILES_ACTIVE: postgres

      # In the Compose network, "postgres" is the service name (DNS name), not localhost
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog

    # But this is still only startup order, not database readiness
    depends_on:
      - postgres

And after such a configuration, it is especially frustrating to see a failure: “How is that possible? Everything is written correctly!” But “written correctly” is about configuration, and today we are talking about timing. And timing is a separate source of pain that cannot be fixed by rewriting SPRING_DATASOURCE_URL.

2. Startup order and readiness

Before we look at logs and “symptoms”, we need to put two labels in our heads. In the Docker/Compose world, it is very easy to call both states “ready” and then wonder why “ready” does not work. Today we will break this habit and distinguish startup order from readiness, because without that the next part of the course will look like a set of rituals.

In everyday language, startup order means “who starts first”. That is, the order in which Compose launches containers. It is about who gets docker run first in an automated way. Readiness (or ready-state) means “who is actually capable of doing the work right now”. For PostgreSQL, that means being ready to accept connections and respond to them, not just the fact that the postgres process has already begun to start up somewhere there.

To reinforce the difference, it is helpful to compare these concepts not “in a vacuum”, but through observable signals.

What we are discussing What it means in practice How it usually shows up
Startup order Compose started the container In docker compose ps the container quickly becomes Up
Readiness The service inside the container is ready to serve clients The logs show that the service reached a “ready” state, but it may take seconds/tens of seconds

If you want a real-life analogy, startup order is “you arrived at work and turned on the coffee machine”. It turned on, the lights came on — that is Up. But the coffee is not ready yet: it needs to warm up, build pressure, and flush the system. That is readiness. And if at that moment you try to “take coffee” (that is, Spring Boot tries to open a JDBC connection), you will get roughly the same result as our app-container life: disappointment and errors.

It is also important to understand the psychological detail: the Up status is often perceived as “it works”. But Up is only “the container process is alive”. It makes no promises about protocols, ports, readiness, successful connections, or internal service initialization.

3. The startup race between app + postgres in real life

Now let us look at the most typical scenario. It is unpleasant precisely because sometimes it “just works”, and sometimes it breaks. That creates the impression that Compose is “random”. In reality, Compose is not random — the startup timing of two processes in different containers is random. This is called a startup race condition: a startup race where the winner is determined by milliseconds and the mood of your laptop.

Imagine the sequence of events during docker compose up. Compose starts PostgreSQL, almost immediately considers the container “up”, and moves on to starting app. But PostgreSQL inside the container may still be in the stage of “bringing up the system, initializing data directories, warming up, starting listeners”. If this is the very first run with a new volume, the database may be doing initial initialization (initdb) — which takes noticeably longer.

From the outside, it looks something like this:

# A conceptual example: the postgres container is already running, but the service is not ready yet
postgres-1 | database system is starting up
app-1      | Failed to connect to database

Yes, this log example is simplified, but it shows the main point: postgres is already “alive”, but still not “ready”, while app has already honestly tried to connect and got rejected.

And here is the classic container-status picture (notice the semantic trap: the database is Up, the application is Exited):

# Important: "Up" for postgres does not mean "ready to accept connections"
NAME         STATUS
postgres-1   Up
app-1        Exited (1)

What is important to understand here (and it is very useful to say it out loud): the app container did not “break forever”. It simply terminated its main process (the Java process of Spring Boot) because it could not complete the mandatory step at startup — connecting to the database. Containers do not “live on their own”: a container lives exactly as long as the main process lives. Spring Boot crashed → the main process ended → the container ended. This is a normal, predictable model, and we have already seen it in the topic about container lifecycle and exit codes.

To make the picture even clearer, it helps to mentally draw a simple timeline. Compose did everything promised by depends_on: it started the database first. But it did not promise readiness.

sequenceDiagram
    participant C as docker compose
    participant P as postgres container
    participant A as app container

    C->>P: start postgres
    P-->>P: "init / warmup / start listeners"
    C->>A: "start app because postgres container is started"
    A-->>A: Spring Boot startup
    A->>P: try JDBC connect
    P-->>A: "not ready yet: connection refused / starting up"
    A-->>C: "process exits 1"

In this scenario, there is exactly one “bad news”: the stack starts unreliably. But there are actually two “good news” items here. First — it is not a mystery, it is explainable. Second — it can be fixed at the Compose configuration level, without rewriting business code or “dancing” around the JDBC URL.

4. Early Spring Boot failure in postgres mode

When we see that app fails almost immediately, it is tempting to think: “Maybe the application is too nervous; can we make it more patient?” But here it is better to switch to engineering mode: Spring Boot is not “nervous”, it just starts the way it was designed to. In the postgres profile, it must bring up the persistence infrastructure, and for that it needs a live and ready PostgreSQL instance.

Very simplistically, the startup of a Spring Boot application in our mode looks like this: the application brings up the context, assembles the configuration, creates a DataSource, and then tries to open a connection to the database. If the connection is not available, the application often has no reason to continue: without the database it cannot serve the API, because its persistence layer is not ready.

In practice, this is often visible in the logs by characteristic fragments: a connection-pool mention (for example, Hikari), then a connection error, then “Application run failed”. A rough picture (not word for word, but by meaning):

# Typical order: pool starts -> connection attempt -> error -> failure
app-1 | HikariPool-1 - Starting...
app-1 | org.postgresql.util.PSQLException: Connection refused
app-1 | ... Application run failed

And here it is important not to confuse two classes of errors. If you see “Connection refused” exactly at the moment when PostgreSQL is still writing “starting up”, that is an excellent candidate for a race condition. But if PostgreSQL has already been writing “ready to accept connections” for a long time, and the application still cannot connect, then this already looks like a real configuration problem (wrong password, wrong host, wrong port, wrong profile).

Another nuance that beginners often miss: the first PostgreSQL start with a new volume is almost always slower. The database creates the data structure, configures system tables, and brings up internal infrastructure. On later starts, when the volume is already populated, the database usually starts faster. That is where the “flaky” effect comes from: yesterday everything worked because everything was already warmed up, but today after down -v or on a colleague’s machine, the database starts more slowly — and the race is lost again.

5. Diagnosis: race or config

When a service fails at startup, your hands naturally reach for the config: rewrite SPRING_DATASOURCE_URL, change the port, add localhost (please do not), change the profile, “just in case” rebuild the image. The problem is that in a race condition, the config may be correct, and the failure will still happen — simply because the database was not ready yet.

Here we need calm diagnosis, without magic. And it is, by the way, very short: we just look at two log streams and check the database state at the moment the application failed. The most useful thing you can do is read the logs at the same time, synchronizing events by time. That is exactly why yesterday we learned not to look only at app.

The minimal set of commands (without flag overload) usually looks like this:

docker compose ps                 # quickly see which containers are Up/Exited
docker compose logs -f app postgres # correlate app and postgres logs by time

With the first command, we quickly see “who is alive, who is dead”. With the second, we see the chronology: what postgres wrote, when app started, and what exactly happened at the moment of failure.

And this gives you the very same “engineering fork in the road”:

If in the PostgreSQL logs you see that at the moment the application failed it is still saying “starting up”, then the problem is very likely timing-related. If the PostgreSQL logs already contain the phrase that it is “ready to accept connections”, but the application still cannot connect, then this is more likely a broken configuration (wrong user/password/database, wrong host, wrong profile). The important thing is not to “guess”, but to look at the signal.

Another trick that helps the brain not to panic: with a race condition, a repeated stack startup (without changing the configuration) very often “fixes the problem” out of nowhere. That is bad news from a reproducibility standpoint, but great news from a diagnostic one. If your setup “sometimes fails, sometimes does not fail” with the same configuration, that is almost always the smell of a startup race.

And conversely: if it fails consistently and in exactly the same way every time, then it is more often a config issue. In that case, depends_on has nothing to do with it: it could not “break” a correct URL, it could only “fail to wait”.

Goal: a dependency readiness signal

Here it is important to pin down the main idea as practically as possible. We need a stack that starts predictably. In a predictable stack, the application starts when the database is not just launched, but actually ready. That is the readiness model: we want to tie the startup of app to the readiness of postgres, not to timing.

We already know how to say this without embarrassment and without “it kind of works”: depends_on gives startup order, but not readiness. Hence the startup race condition. Once you learn to formulate it this way, you stop treating the symptom with painkillers. You no longer “pick a few seconds”, rewrite the JDBC URL at random, or turn your application into a “patient kitty” in business code.

Now we need an observable signal on the PostgreSQL side itself: not just the fact that the container is running, but a real check that “connections can be accepted”. Without such a signal, everything again collapses into guessing the timing.

6. Typical mistakes at this point

Mistake #1: assuming that depends_on “waits for the database”.
This is the most common trap, because the wording “depends_on” sounds like “depends on”, and the brain automatically fills in “so it will wait”. In reality, the short form of depends_on controls only container startup order, not PostgreSQL readiness to accept connections.

Mistake #2: looking only at docker compose ps and concluding “Postgres is Up”.
The Up status is not “ready for work”, but “the container process is alive”. For a database, this is especially tricky: it can be Up, but still be starting up internally. If the application failed, always look at the postgres logs at the same moment in time, otherwise you are investigating the case by looking at only one half of the evidence.

Mistake #3: fixing the race by rewriting SPRING_DATASOURCE_URL.
In a race condition, the configuration may be completely correct, and the problem may be timing. Because of that, attempts to “change the host” often make things worse: you add a real configuration error on top of the timing problem and end up with two different sources of failure that feel the same.

Mistake #4: treating one successful startup as proof that the model is correct.
Sometimes the stack starts, and you want to happily commit the Compose file as “working”. But if startup is unstable and depends on machine speed, volume state, and other randomness, you are just kicking the problem into the future (and usually onto a colleague’s laptop). Here it is important to think not “it started”, but “it starts predictably”.

Mistake #5: trying to fix readiness with delays like sleep 10.
Such “treatment” seems simple, but it does not guarantee anything: on one machine 10 seconds will be enough, on another it will not, and then everything turns into a lottery again. If you are going to solve the problem, do it through an observable service-readiness signal, not by guessing the time.

1
Zadanie
Docker for Spring, poziom 17, lekcja 0
Niedostępne
Niestabilny start z krótkim `depends_on`
Niestabilny start z krótkim `depends_on`
1
Zadanie
Docker for Spring, poziom 17, lekcja 0
Niedostępne
Dwa pliki Compose tylko dla kolejności uruchamiania
Dwa pliki Compose tylko dla kolejności uruchamiania
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION