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
apicontainer starts. - The
dbcontainer also starts. - The
apiapplication boots up very quickly and immediately tries to connect to the database. - However, the database process inside the
dbcontainer 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
apicontainer 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.

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:
healthcheck: A directive indocker-compose.ymlthat tells Docker how to test if the application inside a container is working correctly.depends_on(long syntax): An expanded version ofdepends_onthat allows you to specify aconditionto 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:
-
healthcheckin thedbservice:test: The command to run to check the health.pg_isreadyis a utility that comes with PostgreSQL and returns0if 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_onin theapiservice:- We now use the object syntax (
db:instead of- db). condition: service_healthy: This tells Docker Compose to wait until thedbservice's health check passes before starting theapiservice.
- We now use the object syntax (
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 of0. 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:
frontend: An Nginx web server that talks to theapi.api: A backend application that talks to thedb.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_onshort syntax (- service_name) controls the container start order but does not wait for the application inside to be ready. - The
healthcheckdirective allows Docker to periodically test if an application within a container is functioning correctly. - Combining
healthcheckwith thedepends_onlong 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.