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
Add a health check to the dependency service.
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.
Run the docker compose services, including the dependent service. It will start only when its dependencies are ready.
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.