Notifications about unhealthy Docker containers
A container with a healthcheck can quietly go unhealthy and keep “running”,
accepting traffic but returning errors. And a container with restart: always
can spin in a restart loop for hours — Docker dutifully restarts it, while users
see timeouts. Both cases are easy to miss: docker ps shows the container as
running. Better to get a push than to read complaints.
What we’ll do
Section titled “What we’ll do”The script goes through all running containers every 2 minutes and:
- if a container is in the
unhealthystate — sends a high-priority message; - if the restart count grew since the last check (the container is looping) — also sends a high-priority message;
- when the container is
healthyagain and restarts have stopped — sends a “green” recovery message.
To avoid receiving the same message every 2 minutes, we store the last health and
restart count per container in /var/lib/notifly-docker/ — in spirit these are the
same state files as in the disk recipe.
Script
Section titled “Script”Save as /usr/local/bin/notifly-docker-check:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +a
STATE_DIR=/var/lib/notifly-dockermkdir -p "$STATE_DIR"
HOST=$(hostname -s)
# Names of all running containersdocker ps --format '{{.Names}}' | while read -r NAME; do [ -n "$NAME" ] || continue
# Health: healthy / unhealthy / starting / none (if no healthcheck is defined) HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$NAME" 2>/dev/null || echo none) # How many times Docker has restarted the container RESTARTS=$(docker inspect --format '{{.RestartCount}}' "$NAME" 2>/dev/null || echo 0)
KEY=$(echo "$NAME" | tr '/' '_') HEALTH_FILE="$STATE_DIR/$KEY.health" RESTART_FILE="$STATE_DIR/$KEY.restarts"
LAST_HEALTH=$(cat "$HEALTH_FILE" 2>/dev/null || echo none) LAST_RESTARTS=$(cat "$RESTART_FILE" 2>/dev/null || echo "$RESTARTS")
# 1. Container just became unhealthy (was something else) if [ "$HEALTH" = "unhealthy" ] && [ "$LAST_HEALTH" != "unhealthy" ]; then /usr/local/bin/notifly-send \ "⚠️ Контейнер $NAME: unhealthy — $HOST" \ "Healthcheck контейнера $NAME провалился. Рестартов всего: $RESTARTS." 8 fi
# 2. Restart count grew — the container is looping if [ "$RESTARTS" -gt "$LAST_RESTARTS" ]; then DELTA=$((RESTARTS - LAST_RESTARTS)) /usr/local/bin/notifly-send \ "🔥 Контейнер $NAME перезапускается — $HOST" \ "Новых рестартов: $DELTA (всего $RESTARTS). Похоже на restart-loop, проверьте docker logs $NAME." 9 fi
# 3. Recovery: healthy again and restarts not growing if [ "$HEALTH" = "healthy" ] && [ "$LAST_HEALTH" = "unhealthy" ]; then /usr/local/bin/notifly-send \ "✅ Контейнер $NAME снова здоров — $HOST" \ "Healthcheck контейнера $NAME снова проходит." 4 fi
echo "$HEALTH" > "$HEALTH_FILE" echo "$RESTARTS" > "$RESTART_FILE"doneMake it executable:
sudo chmod +x /usr/local/bin/notifly-docker-checkYou can inspect container state by eye with the same format the script uses:
docker ps --format '{{.Names}} {{.Status}}'In the Status column, containers with a healthcheck show (healthy) or
(unhealthy) in parentheses.
Run on schedule
Section titled “Run on schedule”Using cron
Section titled “Using cron”sudo crontab -eAdd the line:
*/2 * * * * /usr/local/bin/notifly-docker-check >/dev/null 2>&1Using systemd timer (recommended)
Section titled “Using systemd timer (recommended)”/etc/systemd/system/notifly-docker.service:
[Unit]Description=Notifly docker health check
[Service]Type=oneshotExecStart=/usr/local/bin/notifly-docker-check/etc/systemd/system/notifly-docker.timer:
[Unit]Description=Run notifly-docker every 2 minutes
[Timer]OnBootSec=2minOnUnitActiveSec=2min
[Install]WantedBy=timers.targetActivate it:
sudo systemctl daemon-reloadsudo systemctl enable --now notifly-docker.timersudo systemctl list-timers notifly-docker.timerWindows: PowerShell + Task Scheduler
Section titled “Windows: PowerShell + Task Scheduler”Docker Desktop on Windows keeps containers inside WSL2, but the CLI works exactly the
same way — we poll docker ps and docker inspect from PowerShell. The script uses
the common function Send-Notifly from the
sysadmin template/index.
. C:\scripts\Notifly.ps1
$StateDir = "C:\ProgramData\Notifly\docker-state"New-Item -ItemType Directory -Path $StateDir -Force | Out-Null$Host = $env:COMPUTERNAME
# Names of all running containersdocker ps --format '{{.Names}}' | Where-Object { $_ } | ForEach-Object { $name = $_
# Health and restart count via docker inspect $health = docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' $name 2>$null if (-not $health) { $health = "none" } $restarts = [int](docker inspect --format '{{.RestartCount}}' $name 2>$null)
$key = $name -replace '/', '_' $healthFile = Join-Path $StateDir "$key.health" $restartFile = Join-Path $StateDir "$key.restarts"
$lastHealth = if (Test-Path $healthFile) { (Get-Content $healthFile -Raw).Trim() } else { "none" } $lastRestarts = if (Test-Path $restartFile) { [int](Get-Content $restartFile -Raw) } else { $restarts }
# 1. Container just became unhealthy if ($health -eq "unhealthy" -and $lastHealth -ne "unhealthy") { Send-Notifly -Title "⚠️ Контейнер $name`: unhealthy — $Host" ` -Message "Healthcheck провалился. Рестартов всего: $restarts." -Priority 8 }
# 2. Restart count grew — restart loop if ($restarts -gt $lastRestarts) { $delta = $restarts - $lastRestarts Send-Notifly -Title "🔥 Контейнер $name перезапускается — $Host" ` -Message "Новых рестартов: $delta (всего $restarts). Проверьте docker logs $name." -Priority 9 }
# 3. Recovery if ($health -eq "healthy" -and $lastHealth -eq "unhealthy") { Send-Notifly -Title "✅ Контейнер $name снова здоров — $Host" ` -Message "Healthcheck снова проходит." -Priority 4 }
$health | Set-Content $healthFile $restarts | Set-Content $restartFile}Register in Task Scheduler (as administrator, every 2 minutes as SYSTEM):
$Action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Docker-Check.ps1"$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) ` -RepetitionInterval (New-TimeSpan -Minutes 2)$Princ = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel HighestRegister-ScheduledTask -TaskName "Notifly Docker Check" ` -Action $Action -Trigger $Trigger -Principal $Princ -Description "Notifly: проверка контейнеров"Benefits
Section titled “Benefits”- Catch silent failures: an
unhealthycontainer still shows up indocker psas “Up”, but actually stops serving requests — without healthcheck monitoring this is easy to overlook. - Restart loops are visible immediately: a growing
RestartCountcatches containers that crash and restart in a loop long before users notice. - No spam: state is stored per container, so as long as
unhealthydoesn’t change there are no new notifications, and recovery arrives exactly once.
What to improve next
Section titled “What to improve next”- Enrich the message with a log tail:
docker logs --tail 20 $NAME— pass it via theextrasfield to see the cause right away. - Separately notify about containers that disappeared from
docker psentirely (dropped toexited) — similar to the systemd services recipe. - Split prod / staging across separate Notifly channels so they get different sounds.