1. Overview

Docker Compose is a tool for deploying, running, and maintaining multiple-container applications. Through the compose file, we can define many containers that work together to deliver the functionality of an application. One useful feature of Docker Compose is the ability to specify dependencies between different containers in the same setup. This allows us to define the start-up sequence of the containers.

In this tutorial, we’ll learn about service dependencies in Docker Compose and how to utilize it in a setup that involves a MySQL container.

2. Health Check and Readiness of Container

A health check checks if a system is working properly by running a specific command and checking the response. Concretely, an external system, the prober, runs the command on the system and decides if the system is healthy based on the response it gets. For example, a health check on a website involves sending a GET request and expecting it to respond with status code 200 if it’s working.

In Docker, defining a health check for containers unlocks a new dimension to the container’s lifecycle. Specifically, a health check allows the Docker engine to determine if a container is Healthy or Unhealthy in addition to Created, Started, and Stopped. These additional statuses allow finer control of the container, such as only routing traffic to the container when its status is Healthy. This is particularly useful for a container that requires some time to start up after it’s Started.

To provide a health check command, we can specify the HEALTHCHECK instruction in the Dockerfile when we build our image:

$ cat Dockerfile
FROM mysql:5.7

ENV MYSQL_ROOT_PASSWORD=example

HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD mysqladmin ping -h localhost || exit 1

Alternatively, we can specify a health check command in the healthcheck field in our compose file, docker-compose.yaml for Docker Compose deployment:

$ cat docker-compose.yaml
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: example
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

The health check configuration above means that on a 10-second interval, we’ll run the command in the test field to assert the container’s health.  Then, the timeout field specifies that we’ll wait five seconds for a response for each attempt. Finally, the retries field configures a maximum retries count of five before declaring the container unhealthy.

3. Docker Compose Service Dependency

For applications with multiple containers, there could be dependencies between the containers that require the different containers to start up in a particular sequence. For instance, the web application container might run database migration files as part of its start-up logic. This means the web application container requires the database container to be ready for it to start up successfully.

To support dependencies between containers, the Compose specification defines a field, depends_on that we can specify on the service definition. For example, consider a two-service setup, where serviceA should start after serviceB. We can use the depends_on syntax on the serviceA definition in the Docker compose file to express that dependency:

$ cat docker-compose.yaml
services:
  serviceA:
    image: alpine:latest
    depends_on:
      - serviceB

  serviceB:
    image: alpine:latest

When we bring up the service, the Docker engine creates and runs the serviceB first. Once the serviceB status is Running, the Docker engine creates and runs the serviceA.

The depends_on syntax supports a nuanced dependency expression using the condition field. The three possible values are service_startedservice_ready, and service_completed_successfully. These different values dictate how the Docker engine considers if a dependency is satisfied. Let’s look at the difference between each of them.

3.1. service_started

If unspecified, the default condition value is service_started. With the service_started condition in our dependency, the Docker engine considers the dependency satisfied when the dependent service’s status is Started. In other words, the dependency is considered met once the dependent container starts running.

3.2. service_ready

Then, the service_ready condition requires the dependent service to be in Healthy status before considering the dependency as met. Contrary to the default service_started, the service_ready condition waits until the dependent service is Started and Healthy. Critically, this condition requires the dependent service to specify the healthcheck field. Without the healthcheck field, the Docker engine cannot determine when a container is ready.

3.3. service_completed_successfully

Finally, the service_completed_successfully condition requires the dependent service to run to completion. In other words, the dependent service must exit with status code 0 to meet the condition. This is typically used when there’s a separate container for initializing the environment before the application can run. These initializing containers are usually short-lived and run to completion.

4. MySQL Container Readiness

MySQL is a popular relational database management system (RDBMS). For database systems like MySQL, we consider the system ready when it starts accepting external connection requests to perform some commands. The simplest way to assert such a condition is to run an SQL command and expect it to be successful.

Importantly, the SQL command should ideally be side-effect-free as it is executed continuously. One such example is to run the SHOW DATABASES SQL command:

$ mysql -u root -pexample --execute 'SHOW DATABASES;'
+--------------------+
| Database           |
...

If the SQL fails to execute, most likely it’s because the database is not yet accepting the connection. Therefore, we’ll fail the health check. Otherwise, we know the container is ready for connection and set its status to ready.

Let’s look at how to combine what we learned about depends_on and healthcheck to enforce the start-up sequence involving a MySQL container.

4.1. Demonstration: Container Start-up Dependency

The setup consists of two containers: the web and the db. The web container will act as our application container that requires the database to be up and running as part of its start-up process. Additionally, the db container runs the MySQL Docker image:

$ cat docker-compose.yaml
services:
  web:
    image: alpine:latest
    depends_on:
      db:
        condition: service_healthy
  db:
    image: mysql:5.7
    ports:
      - "3306:3306"  # Exposes port 3306 from the container to port 3306 on the host
    environment:
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "mysql", "-u", "root", "-proot", "--execute", "SHOW DATABASES;"]
      interval: 3s
      retries: 5
      timeout: 5s

There are two important items in the manifest file to note. Firstly, we use the depends_on field on the web service to make the web container depend on the db container. Specifically, we’re saying the web container must start after the db container is ready using the service_healthy condition.

Then, we configure the healthcheck field of the db container. This health check lets the Docker engine know if the db container is healthy.

4.2. Seeing Health Check in Action

Let’s bring up the stack using the docker compose up command and see the service dependency in action:

$ docker compose up -d
[+] Running 1/3
 ✔ Network mysql-complete_default  Created                                             0.0s
 ✔ Container mysql-complete-db-1   Created                                             0.1s
 ✔ Container mysql-complete-web-1  Created                                             0.1s

The first thing we observe after we run the command is that both containers go into the Created state. This indicates that the Docker engine has created the containers, and the network object connecting those two containers. Notably, the dependencies requirements only enforce sequence for container execution, not creation.

Later, for a brief moment, we can see that the mysql-complete-db-1 turns into a Running state before it stays in the Waiting state:

[+] Running 2/3
 ✔ Network mysql-complete_default  Created                                             0.0s
 ⠼ Container mysql-complete-db-1   Waiting                                             2.5s
 ✔ Container mysql-complete-web-1  Created                                             0.1s

The Waiting state indicates that the Docker engine is waiting for the container to be healthy. In this state, the Docker engine constantly runs the health check command on the container and waits for it to pass.

When the container is healthy, the container’s status turns to Healthy. Finally, we observe that the mysql-complete-web-1 status turns to Started, which indicates that the container is now running after the dependency is met:

[+] Running 3/3
 ✔ Network mysql-complete_default  Created                                             0.0s
 ✔ Container mysql-complete-db-1   Healthy                                            10.0s
 ✔ Container mysql-complete-web-1  Started                                            10.3s

5. Conclusion

In this tutorial, we’ve first learned about the health check concept in the context of Docker containers. Then, we’ve seen how the depends_on syntax on Docker Compose can work together with the healthcheck syntax to create a start-up dependency. Then, we learned the command for performing health checks on the MySQL system. Finally, we’ve demonstrated how to create service dependencies in the Docker compose file to control the start-up sequence.