Docker Compose Health Check Patterns - Portable Best Practices
Overview
This guide documents portable health check patterns for Docker Compose services. Following these patterns ensures health checks work across different base images, container runtimes, and environments without relying on utilities that may not be present.Table of Contents
- Why Portable Health Checks Matter
- Common Pitfalls
- Portable Patterns by Service Type
- Health Check Configuration Guidelines
- Testing Health Checks
- Troubleshooting
Why Portable Health Checks Matter
The Problem
Many Docker images use minimal base images (Alpine, distroless, scratch) that:- ❌ Lack common utilities (
curl,wget,nc,telnet) - ❌ Don’t include health check tools (
grpc_health_probe,httpie) - ❌ May not have shell interpreters (
/bin/bash,/bin/sh) - ❌ Minimize attack surface by removing non-essential binaries
The Solution
Use service-native health check commands that are guaranteed to exist in the container:- ✅ Database clients (
pg_isready,redis-cli,mongosh) - ✅ Service-specific health endpoints (
kc.sh show-config) - ✅ Built-in health check binaries shipped with the service
- ✅ TCP port checks as fallback (most portable)
Benefits
- Reliability: Health checks don’t fail due to missing utilities
- Portability: Works across different image variants (alpine, distroless, etc.)
- Security: Doesn’t require installing additional packages
- Performance: Uses lightweight native commands
- Maintainability: Survives base image updates
Common Pitfalls
❌ Anti-Pattern 1: Assuming curl Exists
- Alpine-based images:
curlnot installed by default - Distroless images: No package manager to install utilities
- Security-hardened images: Utilities removed to reduce attack surface
qdrant:v1.15.1- lackscurl,wget,grpc_health_probequay.io/keycloak/keycloak:latest- minimal image withoutcurl
❌ Anti-Pattern 2: Using wget Without Verification
❌ Anti-Pattern 3: Hardcoded Shell Paths
❌ Anti-Pattern 4: External Dependencies
Portable Patterns by Service Type
PostgreSQL
✅ Recommended: Usepg_isready
pg_isreadyis always included in official PostgreSQL images- Checks database readiness, not just process existence
- Returns proper exit codes (0 = healthy, 1/2 = unhealthy)
Redis
✅ Recommended: Useredis-cli ping
redis-clialways bundled with Redispingcommand is lightweight and fast- Returns
PONGon success (exit code 0)
Keycloak
✅ Recommended: Use built-inkc.sh command
kc.shis the native Keycloak management scriptshow-configverifies configuration loadedgrep -q 'kc.db'confirms database configured- No external utilities required
MongoDB
✅ Recommended: Usemongosh or mongo client
OpenFGA (gRPC Services)
✅ Recommended Option 1: Use bundledgrpc_health_probe
- Verify
grpc_health_probeis in the image - Check with:
docker run --rm <image> ls /usr/local/bin/grpc_health_probe
- No external utilities needed (uses Bash built-in
/dev/tcp) - Works on any image with Bash
- Checks if port is listening
Qdrant (Vector Database)
✅ Recommended: TCP port check- Qdrant minimal image has no HTTP clients
- Installing utilities defeats minimal image purpose
- Port listening = service ready for most use cases
Elasticsearch
✅ Recommended: Usecurl if available, fallback to TCP
RabbitMQ
✅ Recommended: Userabbitmqctl
Health Check Configuration Guidelines
Timing Parameters Best Practices
interval >= timeout(required by Docker Compose validation)start_periodshould cover typical startup timeretriesshould account for temporary failures (network blips)
| Service | Interval | Timeout | Retries | Start Period | Rationale |
|---|---|---|---|---|---|
| Redis | 3s | 2s | 10 | 5s | Fast startup, lightweight check |
| PostgreSQL | 5s | 3s | 10 | 10s | Moderate startup, db initialization |
| Keycloak | 5s | 5s | 40 | 45s | Slow startup, complex initialization |
| OpenFGA | 3s | 3s | 15 | 5s | Fast startup, gRPC ready quickly |
| Qdrant | 5s | 3s | 10 | 10s | Moderate startup, index loading |
Exit Codes
Health check commands must return proper exit codes:- 0: Healthy (container ready)
- 1: Unhealthy (container not ready or failed)
Shell vs. Exec Form
CMD-SHELL Form (requires shell):- Runs command through
/bin/sh -c - Required for: pipes, redirections, variable expansion
- Risk: Fails if
/bin/shdoesn’t exist (distroless images)
- Executes command directly (no shell)
- Preferred when possible (more portable)
- Works in distroless/minimal images
Testing Health Checks
Verify Health Check Works
1. Start service and monitor health status:Test in Different Image Variants
Validate Timing Parameters
Troubleshooting
Health Check Never Becomes Healthy
Symptom: Container stays instarting or unhealthy state
Debug steps:
-
Check if command exists:
-
Run health check manually:
-
Check service is actually running:
-
Verify ports are listening:
-
Check start_period is sufficient:
Health Check Command Not Found
Symptom:executable file not found or command not found
Solutions:
-
Verify command path:
-
Check shell availability:
-
Use absolute paths:
Health Check Times Out
Symptom: Health checks fail with timeout Solutions:-
Increase timeout:
-
Use faster health check:
-
Check network latency:
Permission Denied Errors
Symptom: Health check fails with permission errors Solutions:-
Run as correct user:
-
Check file permissions:
-
Use sudo if available:
Decision Tree: Choosing the Right Health Check
Complete Examples
Example 1: PostgreSQL with Optimal Settings
Example 2: Redis with Auth
Example 3: Keycloak with Slow Startup
Example 4: Multi-Service with Dependencies
References
- Docker Compose Health Check Reference
- Docker Health Check Best Practices
- Minimal Container Images Security Guide
- OpenAI Codex Finding: Docker Compose health check utilities warning
- Project:
docker-compose.test.yml- Production health check patterns
Summary: Quick Reference
| Service | Best Health Check | Why |
|---|---|---|
| PostgreSQL | pg_isready -U postgres | Native, always available |
| Redis | redis-cli ping | Bundled client, fast |
| Keycloak | kc.sh show-config | grep -q 'kc.db' | Native command, no curl needed |
| OpenFGA | grpc_health_probe -addr=:8081 | gRPC standard |
| Qdrant | timeout 1 bash -c '</dev/tcp/localhost/6333' | No utilities in image |
| MongoDB | mongosh --eval "db.adminCommand('ping')" | Native client |
| RabbitMQ | rabbitmqctl status | Service management tool |
| Generic HTTP | timeout 1 bash -c '</dev/tcp/localhost/PORT' | Most portable |
- ✅ Use service-native commands when possible
- ✅ Fallback to TCP checks for minimal images
- ✅ interval >= timeout (required)
- ✅ Test in actual container before deploying
- ❌ Don’t assume curl/wget exist
- ❌ Don’t use hardcoded shell paths
Last Updated: 2024-11-17 Maintained By: Infrastructure Team Related:
docker-compose.test.yml, docker-compose.dev.yml