Уведомления о нездоровых Docker-контейнерах
Контейнер с healthcheck может тихо уйти в unhealthy и продолжать «работать»,
принимая трафик, но отдавая ошибки. А контейнер с restart: always может часами
крутиться в цикле рестартов — Docker его честно перезапускает, а пользователи
видят таймауты. Оба случая легко не заметить: docker ps показывает контейнер
как запущенный. Лучше получить push, чем читать жалобы.
Что мы будем делать
Заголовок раздела «Что мы будем делать»Скрипт каждые 2 минуты проходит по всем запущенным контейнерам и:
- если контейнер в состоянии
unhealthy— присылает сообщение с высоким приоритетом; - если счётчик рестартов вырос с прошлой проверки (контейнер зациклился) — тоже присылает сообщение с высоким приоритетом;
- когда контейнер снова
healthyи рестарты остановились — присылает «зелёное» сообщение о восстановлении.
Чтобы не получать одно и то же сообщение каждые 2 минуты, мы храним
последнее здоровье и счётчик рестартов по каждому контейнеру в
/var/lib/notifly-docker/ — по духу это те же файлы состояния, что и в
рецепте про диск.
Сохраните как /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)
# Имена всех запущенных контейнеровdocker ps --format '{{.Names}}' | while read -r NAME; do [ -n "$NAME" ] || continue
# Здоровье: healthy / unhealthy / starting / none (если healthcheck не задан) HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$NAME" 2>/dev/null || echo none) # Сколько раз Docker перезапускал контейнер 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. Контейнер стал unhealthy (а был другим) if [ "$HEALTH" = "unhealthy" ] && [ "$LAST_HEALTH" != "unhealthy" ]; then /usr/local/bin/notifly-send \ "⚠️ Контейнер $NAME: unhealthy — $HOST" \ "Healthcheck контейнера $NAME провалился. Рестартов всего: $RESTARTS." 8 fi
# 2. Счётчик рестартов вырос — контейнер зациклился 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. Восстановление: снова healthy и рестарты не растут 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"doneСделайте исполняемым:
sudo chmod +x /usr/local/bin/notifly-docker-checkПроверить состояние контейнеров глазами можно тем же форматом, что и в скрипте:
docker ps --format '{{.Names}} {{.Status}}'В колонке Status для контейнеров с healthcheck будет (healthy) или
(unhealthy) в скобках.
Запуск по расписанию
Заголовок раздела «Запуск по расписанию»Через cron
Заголовок раздела «Через cron»sudo crontab -eДобавьте строку:
*/2 * * * * /usr/local/bin/notifly-docker-check >/dev/null 2>&1Через systemd timer (рекомендуется)
Заголовок раздела «Через systemd timer (рекомендуется)»/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.targetАктивируем:
sudo systemctl daemon-reloadsudo systemctl enable --now notifly-docker.timersudo systemctl list-timers notifly-docker.timerWindows: PowerShell + Task Scheduler
Заголовок раздела «Windows: PowerShell + Task Scheduler»Docker Desktop на Windows держит контейнеры внутри WSL2, но CLI работает точно так
же — опрашиваем docker ps и docker inspect из PowerShell. Скрипт использует
общую функцию Send-Notifly из
шаблона sysadmin/index.
. C:\scripts\Notifly.ps1
$StateDir = "C:\ProgramData\Notifly\docker-state"New-Item -ItemType Directory -Path $StateDir -Force | Out-Null$Host = $env:COMPUTERNAME
# Имена всех запущенных контейнеровdocker ps --format '{{.Names}}' | Where-Object { $_ } | ForEach-Object { $name = $_
# Здоровье и счётчик рестартов через 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. Контейнер стал unhealthy if ($health -eq "unhealthy" -and $lastHealth -ne "unhealthy") { Send-Notifly -Title "⚠️ Контейнер $name`: unhealthy — $Host" ` -Message "Healthcheck провалился. Рестартов всего: $restarts." -Priority 8 }
# 2. Счётчик рестартов вырос — restart-loop if ($restarts -gt $lastRestarts) { $delta = $restarts - $lastRestarts Send-Notifly -Title "🔥 Контейнер $name перезапускается — $Host" ` -Message "Новых рестартов: $delta (всего $restarts). Проверьте docker logs $name." -Priority 9 }
# 3. Восстановление 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}Регистрация в Task Scheduler (от администратора, каждые 2 минуты от 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: проверка контейнеров"- Ловите тихие поломки: контейнер в
unhealthyпродолжает висеть вdocker psкак «Up», но реально не обслуживает запросы — без healthcheck-мониторинга это легко проглядеть. - Restart-loop виден сразу: рост
RestartCountловит контейнеры, которые падают и перезапускаются по кругу, задолго до того, как это заметят пользователи. - Без спама: состояние по каждому контейнеру хранится отдельно, поэтому
пока
unhealthyне изменился — новых уведомлений нет, а восстановление приходит ровно один раз.
Что улучшить дальше
Заголовок раздела «Что улучшить дальше»- Дополнить сообщение хвостом логов:
docker logs --tail 20 $NAME— передавайте через полеextras, чтобы сразу видеть причину. - Отдельно уведомлять о контейнерах, которые вообще исчезли из
docker ps(упали вexited) — аналогично рецепту про systemd-сервисы. - Разнести prod / staging по разным каналам Notifly, чтобы у них были разные звуки.