Skip to main content

Welcome to a Grasp lesson

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

Lesson 25

Health Checks and Dependencies in Docker Compose

Recommended resources

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 as unhealthy (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.
Docker Compose Health Checks and Service Dependencies Example
This image shows a clear example of the YAML syntax for both `healthcheck` and the `depends_on` condition we will cover next. It illustrates a common use case: a web service depending on a healthy database.

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:

  1. 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.
  2. service_healthy: This is what we want for long-running services. The dependent service waits until the dependency's health check passes successfully.
  3. 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.

Docker Compose PS Output with Health Checks
This command-line output shows the result of `docker compose ps`. You can see that the `db`, `redis`, and `vote` services are running and have passed their health checks, indicated by the `(healthy)` status.

If a service is stuck in a starting state or becomes unhealthy, here are a few troubleshooting steps:

  1. Check the logs: Use docker compose logs <service_name> to see if the application inside the container is throwing any errors.
  2. Inspect the health check details: Run docker inspect <container_name> and look for the Health section in the JSON output. It will show you the output of the last few health checks, which is invaluable for debugging a failing test command.
  3. 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:

  1. A healthcheck block was added to the db service. The test uses pg_isready, a standard PostgreSQL utility, to check if the server is ready for connections. A start_period is included to give Postgres time to initialize.
  2. A healthcheck block was added to the cache service. The test uses redis-cli ping, which returns PONG if the server is responsive.
  3. The depends_on block in the web service was converted to the long format, specifying condition: service_healthy for both the db and cache services.

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_on directive only controls the startup order of containers, not their readiness.
  • The healthcheck directive allows you to define a specific command to validate that the application within a container is operational.
  • Using depends_on with condition: service_healthy is the standard, robust method for ensuring a service waits for its dependencies to be fully ready.
  • The condition: service_completed_successfully is 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.