Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 92

Managing Service Dependencies with `depends_on`

Recommended resources

Hello! Welcome to your next lesson on Docker Compose.

In our last session, we secured our application by moving configuration and secrets out of the docker-compose.yml file and into a .env file. This was a critical step for creating professional, version-controlled applications. However, as we hinted, another common problem lurks in multi-container setups: the startup race condition. Imagine your web application starts faster than your database. It tries to connect, fails because the database isn't ready, and then crashes.

Today, we'll solve this exact problem. This lesson's goal is to learn how to control service startup order and dependencies using depends_on. By the end, you'll be able to create robust multi-service applications that start up reliably every time.

1. The Problem: The Startup Race Condition

In a multi-service application, services often rely on each other. A web server needs its backend API, and the backend API needs its database. When you run docker compose up, Docker starts all the containers for your services concurrently, but it doesn't know about these internal application dependencies.

This can lead to a "race condition":

  • The api container starts.
  • The db container also starts.
  • The api application boots up very quickly and immediately tries to connect to the database.
  • However, the database process inside the db container is still initializing (creating files, checking integrity, etc.) and is not yet ready to accept connections.
  • The API's connection attempt fails, causing the application to crash and the api container to exit.

This can be frustrating because sometimes it might work (if the DB wins the race) and sometimes it might fail, leading to an unreliable development environment.

The following image shows a common dependency structure where a server depends on a database. We need a way to tell Docker Compose about this relationship.

Docker Compose Services with `depends_on` and Volumes
Caption: This diagram illustrates a `zabbix-server` service that has a dependency on the `zabbix-db` service. The `depends_on` key is used in the `docker-compose.yml` file to formalize this startup relationship.

To understand the problem visually and see why simply starting one container before another isn't enough, it's helpful to read the introduction of the following article.

Wait! My Container is Crashing Before Startup

The article 'Wait! My Container is Crashing Before Startup' begins by perfectly illustrating the timing issue between container startup and application readiness. The diagrams are especially useful for visualization.

Read just the introductory section titled 'The Problem'. Pay close attention to the two diagrams that show 'Concurrent container startup' and 'Sequential container startup'. Notice how even in the sequential scenario, the application in Container 1 can still make a request before the application in Container 2 is ready.


2. The Simple Solution: The depends_on Short Syntax

Docker Compose provides a simple way to define the startup order with the depends_on key. This is known as the "short syntax".

Let's say we have a web service that depends on a redis cache.

services:
  web:
    build: .
    depends_on:
      - redis # <-- This service will wait for the 'redis' service to start
  redis:
    image: redis:alpine

What this does: Docker Compose guarantees that the redis container will be started before the web container is started.

What it does NOT do: It does not wait for the Redis application inside the redis container to be fully initialized and ready to accept connections. For many applications, this is the crucial missing piece. You're just winning the "container start" race, not the "application ready" race.

This video gives a clear explanation of why we need depends_on and how the basic form works.

Docker Compose Beginner Level Guide with multiple examples | Docker Compose is Easy

In this clip from 'Docker Compose Beginner Level Guide', the presenter explains the importance of managing dependencies and introduces the depends_on parameter.

Watch from 38:41 to 41:36. The presenter uses an Nginx load balancer depending on backend web services to illustrate the point. Note his emphasis on 'there is no point of having engine up and running... if web one... fails.'

While this simple form is an improvement, it's not a complete solution for services that take time to initialize, like databases.


3. The Robust Solution: depends_on with Health Checks

To truly solve the problem, we need to tell our web service to wait until the db service is not just started, but healthy. We can achieve this by combining two features:

  1. healthcheck: A directive in docker-compose.yml that tells Docker how to test if the application inside a container is working correctly.
  2. depends_on (long syntax): An expanded version of depends_on that allows you to specify a condition to wait for.

Here’s how they work together in our docker-compose.yml for a backend and a PostgreSQL database:

services:
  api:
    build: ./api
    ports:
      - "8000:8000"
    depends_on:
      db: # <-- Name of the service to depend on
        condition: service_healthy # <-- Condition to wait for
    environment:
      DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

  db:
    image: postgres:14-alpine
    environment:
      # These would come from our .env file
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      # This command checks if the Postgres server is ready
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

volumes:
  db-data:

Let's break down the new parts:

  • healthcheck in the db service:

    • test: The command to run to check the health. pg_isready is a utility that comes with PostgreSQL and returns 0 if the server is ready.
    • interval: Wait 5 seconds between checks.
    • timeout: If a check takes longer than 5 seconds, it's considered a failure.
    • retries: If a check fails, Docker will try again up to 5 times.
    • start_period: Gives the container a grace period of 10 seconds to start up before the health checks begin failing it.
  • depends_on in the api service:

    • We now use the object syntax (db: instead of - db).
    • condition: service_healthy: This tells Docker Compose to wait until the db service's health check passes before starting the api service.

This combination is the standard, robust way to manage startup dependencies. The following video provides a fantastic walkthrough of this exact scenario.

What is Docker Compose? Multiple Containers & Docker Networking

This clip from 'What is Docker Compose?' directly addresses the race condition between a backend and a database and demonstrates the complete solution using depends_on with condition: service_healthy and healthcheck.

Watch from 12:11 to 14:01. The video clearly explains the problem and then shows the exact YAML syntax for both the healthcheck and the conditional depends_on.

For a detailed textual guide that walks through this entire process, from the initial problem to the final solution with health checks, the following article is an excellent resource.

How to Use the depends_on Setting in Docker Compose

The article 'How to Use the depends_on Setting in Docker Compose' provides a step-by-step tutorial using a Python and Redis example. It's a great way to reinforce what you've just seen.

Read from the beginning through the 'Condition' subsection. This will cover the problem statement, the short syntax, the long syntax, and an example of adding a healthcheck to a Redis service.


4. Other Conditions and Options

While service_healthy is the most common condition, there are others you might see:

  • service_started: This is the default behavior you get with the short syntax. It waits for the container to start.
  • service_completed_successfully: This waits for a container to run and exit with a code of 0. This is useful for one-off tasks, like a container that runs a database migration script before the main application starts.

The article "Wait! My Container is Crashing Before Startup" includes a helpful cheat sheet that summarizes when to use each synchronization method. It's a great reference to bookmark.

Test your understanding!

You are building a three-tier application with docker-compose. The services are:

  1. frontend: An Nginx web server that talks to the api.
  2. api: A backend application that talks to the db.
  3. db: A MySQL database.

The dependencies are frontend → api → db. The api service has a health check at the /health endpoint, which you can test with curl -f http://localhost:8080/health. The db service is MySQL, which can be checked with mysqladmin ping -h localhost.

How would you modify the docker-compose.yml below to ensure a reliable startup order?

# Initial docker-compose.yml
services:
  frontend:
    image: my-frontend
    ports:
      - "80:80"

  api:
    image: my-api
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: example
Show answer

Here is the corrected docker-compose.yml with the proper dependencies and health check.

services:
  frontend:
    image: my-frontend
    ports:
      - "80:80"
    depends_on:
      api: # Frontend depends on the API
        condition: service_healthy

  api:
    image: my-api
    ports:
      - "8080:8080"
    depends_on:
      db: # API depends on the database
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: example
    healthcheck:
      # Note: We're running this command *inside* the container.
      # `localhost` refers to the container itself.
      test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
      timeout: 20s
      retries: 10

This configuration ensures that db will become healthy first, then api will start and become healthy, and only then will frontend start.


Conclusion

You've now mastered a fundamental skill for building stable multi-container applications. By controlling the startup order and verifying service health, you eliminate an entire class of frustrating, intermittent failures.

Key Takeaways:

  • Docker Compose starts services concurrently, which can cause race conditions if services depend on each other.
  • The depends_on short syntax (- service_name) controls the container start order but does not wait for the application inside to be ready.
  • The healthcheck directive allows Docker to periodically test if an application within a container is functioning correctly.
  • Combining healthcheck with the depends_on long syntax (condition: service_healthy) is the robust and recommended way to manage startup dependencies.

Next Up

Our application is now well-structured, securely configured, and starts up reliably. But what happens when our API gets a lot of traffic? Running a single container might not be enough. In the next lesson, we will learn how to scale individual services up or down using the docker-compose up --scale command, allowing us to handle increased load with ease.