depends_on with condition: service_healthy is only as good as the health check behind it. While testing database containers for a Docker tutorial, I found that three of the most copied health checks report healthy before the service can actually be used. Your app container starts, its first connection fails, and the logs make it look like a network problem.
Everything below was run on Docker Engine 29.9 with Compose v5.6, using mysql:8.4, postgres:18 and redis:8-alpine.
1. MySQL: mysqladmin ping -h localhost
This is the health check you'll find in many examples:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost"]
On a fresh volume:
$ docker compose up -d --wait
✔ Container mysql-demo-db-1 Healthy 5.8s
$ docker compose exec db sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT 1"'
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
$ docker compose logs db 2>&1 | grep -E "Temporary server|port: 3306"
db-1 | [Note] [Entrypoint]: Temporary server started.
Healthy after 5.8 seconds, but root can't log in yet, and the log shows why: the container is still on its temporary server.
On first start (empty data directory), the image's entrypoint starts a temporary mysqld to create users and run the files in /docker-entrypoint-initdb.d/. That temporary server is started with networking disabled (port: 0) and only listens on the Unix socket. Two things then line up badly:
- The MySQL client treats
-h localhostas "use the socket", so the ping reaches the temporary server. -
mysqladmin pingsucceeds as soon as the server answers, even if the login itself would fail.
Fix: force TCP, which the temporary server doesn't accept.
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" --silent"]
interval: 10s
retries: 10
start_period: 30s
With this version, the same project became healthy after about 11 seconds, which matches when the real server logged port: 3306. (The $$ stops Compose from substituting the variable itself, so the shell inside the container reads it.)
2. PostgreSQL: pg_isready without -h
Same idea, different database:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
$ docker compose up -d --wait
✔ Container pg-demo-db-1 Healthy 3.2s
$ docker compose exec db psql -U app -h 127.0.0.1 -c "SELECT count(*) FROM products;"
psql: error: connection to server at "127.0.0.1", port 5432 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
Healthy after 3.2 seconds, while TCP connections are still refused. The Postgres entrypoint also runs your init scripts on a temporary server that listens on the Unix socket only. Without -h, pg_isready uses that socket and reports ready.
Fix:
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U app -d app"]
interval: 2s
timeout: 3s
retries: 30
Now the check went healthy at 11.3 seconds, after PostgreSQL init process complete and listening on IPv4 address "0.0.0.0", port 5432.
3. Redis: redis-cli ping with a password
This one has nothing to do with startup. With requirepass set and a wrong password:
$ docker compose exec -e REDISCLI_AUTH=wrong redis redis-cli ping; echo "exit code: $?"
AUTH failed: WRONGPASS invalid username-password pair or user is disabled.
NOAUTH Authentication required.
exit code: 0
redis-cli prints an error but exits with 0, and Docker only looks at the exit code. So a health check of redis-cli ping passes even when the password is wrong, and your app fails later with NOAUTH.
Fix: check the reply, not the exit code.
$ docker compose exec -e REDISCLI_AUTH=wrong redis sh -c "redis-cli ping | grep -q PONG"; echo "exit code: $?"
AUTH failed: WRONGPASS invalid username-password pair or user is disabled.
exit code: 1
redis:
image: redis:8-alpine
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
environment:
REDISCLI_AUTH: ${REDIS_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
interval: 5s
retries: 10
(Pass the password with the REDISCLI_AUTH environment variable rather than -a, which puts it in the process list and prints a warning.)
How to check your own health checks
A health check is a command whose exit code Docker trusts blindly. Two quick tests catch most problems:
-
Start on an empty volume (
docker compose down -v, thendocker compose up -d --wait) and immediately try a real connection the way your app does: over TCP, with the app's user. If--waitreturns before that works, the check is too optimistic. -
Break the credentials on purpose and run the check by hand with
echo $?. It should exit non-zero.
To see what Docker recorded for each run:
{% raw %}docker inspect --format "{{json .State.Health}}" <container>{% endraw %}
I collected these and other Docker pitfalls, all with real command output, in a free tutorial (also available in Chinese, Japanese and Spanish): codingkit.com/docker. The MySQL, PostgreSQL and Redis chapters have the full Compose files for the examples above.












