Healthcheck aggregator for Docker containers. Healdy exposes a single FastAPI endpoint that groups multiple containers into a named service and reports an aggregated health status.
- Reads a JSON config that maps a service name to a list of container selectors (names, glob patterns, or labels).
- Queries the local Docker Engine for each container's state and healthcheck status.
- Returns
200 OKwhen all containers are healthy, or503when any are missing, unhealthy, or Docker API is unavailable.
- Aggregated health per logical service
- Supports exact names, glob patterns, and label selectors
- Verbose or minimal responses via
VERBOSE - Config file hot-reload when the file changes
docker compose up --buildThen call:
curl http://localhost:18080/healthcheck/keycloakThe provided docker-compose.yml mounts the Docker socket (read-only) and the config file. It exposes the API on port 18080.
By default, Healdy reads the config from /config/services.json. This repo ships an example file at config/config.json, which is mounted by Docker Compose.
Example config:
{
"keycloak": {
"containers": ["keycloak", "keycloak-db"]
},
"observability": {
"containers": ["grafana", "prometheus", "loki"]
},
"webapp": {
"containers": [
"webapp",
"webapp-*",
{"label": "com.docker.compose.service=webapp-worker"}
]
}
}Selectors in containers can be:
- Exact container names (strings)
- Glob patterns (strings using
*,?,[]) - Label selectors (
{"label": "key=value"}or{"label": "key"})
Returns the aggregated health status for the service defined in the config.
Possible responses:
200 OKwhen all containers are healthy503 Service Unavailablewhen any container is unhealthy, starting, missing a healthcheck, missing entirely, or the Docker API is unavailable404 Not Foundwhen the service name is not in the config
Response shape (ok, verbose default):
{
"status": "ok",
"service": "keycloak",
"checked": [
{
"name": "keycloak",
"id": "a1b2c3d4e5f6",
"status": "running",
"health": "healthy",
"exit_code": 0
}
]
}Response shape (degraded, verbose default):
{
"status": "degraded",
"service": "webapp",
"unhealthy": [
{
"name": "webapp-worker",
"id": "f1e2d3c4b5a6",
"status": "running",
"health": "unhealthy",
"exit_code": 1,
"reason": "unhealthy"
},
{
"reason": "missing-container",
"containers": ["webapp-db"]
}
],
"checked": [
{
"name": "webapp",
"id": "abcd1234efgh",
"status": "running",
"health": "healthy",
"exit_code": 0
}
],
"expected": ["webapp", "webapp-*", {"label": "com.docker.compose.service=webapp-worker"}]
}expected echoes the selectors from the config (strings or label objects).
Response shape (minimal, VERBOSE=false):
HTTP status codes remain the same; only the body is reduced to status.
{ "status": "ok" }{ "status": "degraded" }{ "status": "not-found" }Reasons reported in unhealthy:
no-healthcheckwhen a container has no Docker healthcheckunhealthywhen the healthcheck reportsunhealthystarting(or other Docker health states) when the healthcheck is nothealthy- any Docker container status other than
running(e.g.exited,paused,restarting) missing-containerwhen a container selector in the config cannot be found (missing entries may includepattern:orlabel:prefixes)
CONFIG_PATH(default:/config/services.json)DOCKER_TIMEOUTin seconds (default:3)VERBOSE(default:true) acceptstrue/false,1/0,yes/no,on/off
python -m venv .venv
source .venv/bin/activate
pip install -r src/requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8080 --app-dir srcThen call:
curl http://localhost:8080/healthcheck/keycloakIf you place a proxy in front of Healdy, you can map a public /healthcheck endpoint to a specific service. Replace <service_name> with a service key from your config.
Apache2 (mod_proxy):
ProxyPass "/healthcheck" "http://healdy:8080/healthcheck/<service_name>"
ProxyPassReverse "/healthcheck" "http://healdy:8080/healthcheck/<service_name>"Nginx:
location /healthcheck {
proxy_pass http://healdy:8080/healthcheck/<service_name>;
}With this setup you can check the health at https://example.it/healthcheck and keep the service behind the proxy fully transparent. For example, integrating it with Gatus (https://github.com/TwinProduction/gatus) is handy. The public endpoint path is fully customizable in your proxy configuration.
- This service needs access to the Docker API via
/var/run/docker.sock. That grants high privileges on the host; run it in a trusted environment. - Container names in the config must match
docker psnames, not image names. - The config file is cached and reloaded automatically when its file timestamp changes.
Issues and pull requests are welcome. Please include a clear description and reproduction steps when reporting bugs.
MIT. See LICENSE for details.