Перейти к содержимому

Уведомления о нездоровых 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 bash
set -eu
set -a; source /etc/notifly.env; set +a
STATE_DIR=/var/lib/notifly-docker
mkdir -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) в скобках.

Окно терминала
sudo crontab -e

Добавьте строку:

*/2 * * * * /usr/local/bin/notifly-docker-check >/dev/null 2>&1

/etc/systemd/system/notifly-docker.service:

[Unit]
Description=Notifly docker health check
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notifly-docker-check

/etc/systemd/system/notifly-docker.timer:

[Unit]
Description=Run notifly-docker every 2 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=2min
[Install]
WantedBy=timers.target

Активируем:

Окно терминала
sudo systemctl daemon-reload
sudo systemctl enable --now notifly-docker.timer
sudo systemctl list-timers notifly-docker.timer

Docker Desktop на Windows держит контейнеры внутри WSL2, но CLI работает точно так же — опрашиваем docker ps и docker inspect из PowerShell. Скрипт использует общую функцию Send-Notifly из шаблона sysadmin/index.

C:\scripts\Notifly-Docker-Check.ps1
. 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 Highest
Register-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, чтобы у них были разные звуки.