Hello! Welcome to the first lesson in our module on multi-service applications with Docker Compose.
In our last module, we focused on the nuts and bolts of Docker networking and persistence, and wrapped up by learning how to keep our Docker host clean with prune commands. These are all crucial skills for maintaining a tidy and efficient development environment. Now, we'll shift our focus to a higher level: orchestrating multiple services to work together seamlessly.
When you run a multi-service application (like a web app, a database, and a caching service) with docker compose up, Docker starts them all, but it doesn't guarantee the order or wait for them to be ready. This can lead to race conditions: your application might try to connect to the database before the database has finished initializing, causing your app to crash.
Today, we'll solve this classic problem. Our goal is to implement health checks and service dependencies with conditions in Compose. This will ensure that your services not only start but do so in the correct order, waiting until each dependency is genuinely ready to accept connections. For you as a DevOps engineer, mastering this is fundamental to building reliable, resilient containerized systems.
The Problem: Startup Order vs. Readiness
By default, docker-compose up starts all services defined in your docker-compose.yml file concurrently. You might think the depends_on directive solves this, and it's a good first step, but it has a crucial limitation.
Let's start by understanding what depends_on does and, more importantly, what it doesn't do.
Control startup order - Docker Compose
The official Docker documentation provides the most precise definition of depends_on. Let's quickly review its basic functionality.
Read the introductory section just before the 'Control startup' heading. Notice how it states that Compose starts containers in dependency order but doesn't mention anything about waiting for them to be 'ready'.
As the documentation implies, depends_on in its basic form only ensures that the db container is started before the web container. It does not wait for the PostgreSQL process inside the db container to be fully initialized and ready to accept connections.
The following video demonstrates this exact problem and its consequences.
Docker Compose Tutorial for Beginners (Networks - Volumes - Secrets - Postgres - Letsencrypt)
In this clip from the 'Docker Compose Tutorial for Beginners' by Anton Putra, you'll see a clear explanation of why the basic depends_on is often not enough.
Watch the segment from 00:51:18 to 00:52:08. The speaker highlights that depends_on by itself does not check if a service is healthy, and it only works in his specific case because the application driver has its own built-in retry logic.
Relying on application-level retry logic is a workaround, not a solution. A much better approach is to make Docker Compose aware of the health of your services.
Defining "Ready" with Health Checks
To solve the readiness problem, we need a way to ask a container, "Are you actually ready for work?" We do this using the healthcheck directive in our docker-compose.yml.
A health check is a command that Docker runs inside your container periodically. If the command returns a successful exit code (0), the container is marked as healthy. If it returns an error code (1), it's marked as unhealthy.
A healthcheck configuration has several key parameters.
Stop Struggling with Docker Compose – Use These 10 Tricks Instead!
This video, 'Stop Struggling with Docker Compose', gives an excellent breakdown of the healthcheck directive and its parameters. It explains why health checks are so valuable for building robust services.
Watch from 08:42 to 11:13. Pay close attention to the different configuration options: test, interval, timeout, retries, and start_period.
To summarize, the main healthcheck options are:
test: The command to run to check the health. This is the only mandatory option.interval: How often to run the check (e.g.,30s).timeout: How long to wait for the command to return before considering it failed (e.g.,10s).retries: How many consecutive failures are needed to mark the container asunhealthy(e.g.,3).start_period: A grace period for the container to start up before the health check failures are counted (e.g.,40s). This is extremely useful for services like databases that can take a while to initialize.

Connecting Dependencies to Health: The condition Keyword
Now that we can define what it means for a service to be healthy, we can tell other services to wait for that healthy state. We do this by expanding the depends_on syntax to include a condition.
This brings us to the modern, robust way of defining service dependencies.
How to Use Docker Compose depends_on with Health ...
The article 'How to Use Docker Compose depends_on with Health Checks' from oneuptime.com provides a comprehensive guide on this topic. It introduces the condition keyword and provides many practical examples.
Please read the following sections from the article: 'The Problem with Basic depends_on' and 'depends_on with Conditions': This will reinforce the concepts and show the new YAML syntax. 'Available Conditions': Note the difference between service_healthy and service_completed_successfully. 'Health Check Configuration': This serves as a good reference for the health check options. 'Common Service Health Checks': This section is a great resource. Skim through the examples for PostgreSQL, Redis, and HTTP Services. You'll find these snippets very useful in your work.
The three main conditions you will use are:
service_started: This is the default behavior if you don't specify a condition. The dependent service starts as soon as the dependency container is created.service_healthy: This is what we want for long-running services. The dependent service waits until the dependency's health check passes successfully.service_completed_successfully: This is perfect for one-off tasks that need to run before your main application starts. A classic example is running a database migration script. The dependent service will wait for the migration container to run its command and exit with code 0.
Here is a practical example showing how to wait for a Flask application to become healthy before starting an Nginx reverse proxy.
Docker Compose Tutorial for Beginners (Networks - Volumes - Secrets - Postgres - Letsencrypt)
Let's return to Anton Putra's tutorial to see this in action. He refactors his Compose file to use a proper health check with a dependency condition.
Watch from 00:53:52 to 00:55:20. Notice how he adds a healthcheck to the Flask service that uses curl and then modifies the depends_on block in the Nginx service to wait for condition: service_healthy.
Verification and Troubleshooting
Once you've configured your health checks, you need to verify they are working. The docker compose ps command will show the health status in the STATUS column.

If a service is stuck in a starting state or becomes unhealthy, here are a few troubleshooting steps:
- Check the logs: Use
docker compose logs <service_name>to see if the application inside the container is throwing any errors. - Inspect the health check details: Run
docker inspect <container_name>and look for theHealthsection in the JSON output. It will show you the output of the last few health checks, which is invaluable for debugging a failingtestcommand. - Run the command manually: Use
docker compose exec <service_name> <health_check_command>to run your test command directly inside the container and see what it outputs.
Test your understanding!
You are given the following docker-compose.yml for a web application that uses a PostgreSQL database and a Redis cache. The application frequently crashes on startup because it can't connect to the database.
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
depends_on:
- db
- cache
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=app
cache:
image: redis:7-alpine
Modify this file to ensure the web service only starts after both the db and cache services are fully ready and healthy. Use the common health check commands for PostgreSQL and Redis.
Show answer
Here is the corrected docker-compose.yml:
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
Explanation:
- A
healthcheckblock was added to thedbservice. Thetestusespg_isready, a standard PostgreSQL utility, to check if the server is ready for connections. Astart_periodis included to give Postgres time to initialize. - A
healthcheckblock was added to thecacheservice. Thetestusesredis-cli ping, which returnsPONGif the server is responsive. - The
depends_onblock in thewebservice was converted to the long format, specifyingcondition: service_healthyfor both thedbandcacheservices.
Conclusion
In this lesson, you've learned how to build more resilient multi-service applications by moving beyond simple startup order to true readiness checks.
Key Takeaways:
- The basic
depends_ondirective only controls the startup order of containers, not their readiness. - The
healthcheckdirective allows you to define a specific command to validate that the application within a container is operational. - Using
depends_onwithcondition: service_healthyis the standard, robust method for ensuring a service waits for its dependencies to be fully ready. - The
condition: service_completed_successfullyis a powerful pattern for handling initialization tasks like database migrations.
In our next lesson, we will continue building on our Docker Compose skills by learning how to manage environment-specific configurations using .env files and multiple compose files. This will allow you to easily switch between development, testing, and production setups without changing your core Compose file.