At PMG we use Docker and Docker Compose to spin up backing services for local development and continuous integration — think things like databases, cache, or localstack.
One challenge, especially in a CI environment, is making sure that the services are up and available before starting test runs.
Enter HEALTHCHECK
Dockerfiles can contain a HEALTHCHECK stanza, and similarly docker run
also takes a --health-cmd
flag. And, of course, docker compose also allows healthcheck to be defined per service.
The HEALTHCHECK or --health-cmd
runs inside the container to see if, as the name implies, the container is up and healthy. This status becomes available via docker inspect
.
Healthcheck in Docker Compose
Let’s use PostgreSQL as an example here. There is a pg_is
ready command that can check if postgres up and accepting connections.
services:
postgres:
image: postgres:16
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
retries: 1
Now we can combine a little docker inspect
with docker compose ps -q {servicename}
to pull the status:
docker inspect --format "{{.State.Health.Status}}" $(docker compose ps -q postgres)
And throw that into a loop to wait a bit and keep checking, this is best combined into a wait_for_service
script that can be re-used for any service in the compose file:
#!/usr/bin/env bash
set -e
container="$1"
if [ -z "$container" ]; then
echo "usage: $0 service"
exit 1
fi
count=0
alive="no"
while [ "$count" -lt 6 ]; do
health=$(docker inspect --format "{{.State.Health.Status}}" "$(docker-compose ps -q $container)")
if [ "$health" == "healthy" ]; then
alive="yes"
break
fi
echo "waiting for $container: $health"
sleep 10
count=$((count+1))
done
if [ "$alive" = "yes" ]; then
exit 0
else
echo "$container did not start up in time"
exit 1
fi
Now waiting for postgres to be up in a CI environment is a ./wait_for_service postgres
away.
Another Healthcheck Example: Localstack
Not everything comes with a handy pg_isready
equivalent. Localstack, for instance, spins up very quickly, but can take a bit to run it’s initialization hooks. For that use case, I will often have a 999-ready.sh
script in my localstack ready.d that runs last and creates a /tmp/localstack_ready
file:
#!/usr/bin/env bash
echo 'yes' > /tmp/localstack_ready
The healthcheck command can check for this file and mark the container as healthy:
services:
localstack:
image: localstack/localstack:3
ports:
- "4566:4566"
volumes:
- ./docker/localstack/ready.d:/etc/localstack/init/ready.d
healthcheck:
test: ["CMD", "test", "-f", "/var/lib/localstack/localstack_ready"]
retries: 1
And again ./wait_for_service localstack
is all it takes to make sure the backing services and resources are up and ready for the engineer or a CI server.