Docker Compose: Waiting for a Service

Docker containers are now ubiquitous in web development workflows. I often use docker compose to run services that I need during development and in test environments. I often have to wait for a service running in a docker container to be ready before running integration tests.

It's easy to wait a few seconds on my local machine, no problem. But automated tests on a CI server need their wait programmed. At a previous job we used a wait-for-it.sh script to ping the services at intervals until they were all ready. Yesterday, I learned another way to do it using health checks and service dependencies.

The TL;DR

  1. Add a health check to the dependency service.

  2. Add a dependent service that waits on the dependency service using the depends_on service element. Use the long syntax and set the appropriate condition to wait on.

  3. Run the docker compose services, including the dependent service. It will start only when its dependencies are ready.

  4. If the dependent is a test that does not run in a docker compose service, then run the dependencies in the background, run a dummy dependent service in the foreground to exit before your test runs.

Example: a database, a dependent service, and an integration test

Suppose we have a project that needs a database (say MySQL) and a second dependency that also needs the database. We can configure the second service to start after the database is ready as follows.

# file: docker-compose.yaml

name: Hello, world!

services:
  database:
    image: mysql
    ports:
      - "3306:3306"
    environment:
      # --snip--
    healthcheck:
      test: mysql -u test -p test -e "SELECT true"
      interval: 1m
      start_period: 10s
      start_interval: 1s

  other-service:
    image: busybox
    command: echo "I'm running ${COMPOSE_PROJECT_NAME}"
    depends_on:
      database:
        condition: service_healthy
        restart: true

other-service could be a web server, but this suffices. It depends on the database and will start when the database's health check passes. The health check is a simple SELECT true query that verifies that the database can now accept connections.

Now suppose we want to run some tests outside docker when database and other-service are up. Most likely, we'll start both of them in detached mode (running in the background) so that the shell can run the next command for our test. How can we know when both are ready? We can add a third service that does this check for us. This also requires a health check on other-service.

# file: docker-compose.yaml
# --snip-- 
 other-service:
    image: busybox
    command: echo "I'm running ${COMPOSE_PROJECT_NAME}"
    depends_on:
      database:
        condition: service_healthy
    healthcheck:
      test: ping -c 3 http://other-service:8080/health
      interval: 1m
      start_period: 10s
      start_interval: 2s

  ready-for-tests:
    image: busybox
    command: echo "I'm ready for tests"
    depends_on:
      other-service:
        condition: service_healthy

Now we can run database and other-service detached, run ready-for-tests attached, and then run our tests after ready-for-tests exits successfully. Here's a Makefile that may do that.

.PHONY
deps:
    docker compose up -d database other-service

.PHONY
test: deps
    docker compose up ready-for-tests && go test -tags=integration ./...

We have successfully arranged the dependencies in order. ready-for-tests waits for other-service which in turn waits for database. The cool part is that we also improved our setup by adding health checks in the process, so docker can now manage our services better. ready-for-tests is the equivalent of our wait-for-it.sh file, but it is now much simpler. We cannot make any mistake there, unlike in a shell script. Neat!

Bonus tip

Sometimes you want to start a group of services, but not others. This is important if you have many services in your docker-compose.yaml file but not all of them are always needed. Rather than listing out the services to start as in docker compose up [service-1 service-2 ... service-N], you can group them into docker compose profiles and pass the --profile flag to select a group.