Планировщик запускает задачу и молча уходит. Если ночной бэкап упал на середине, узнают об этом обычно в тот момент, когда файлы понадобились. Классический cron умеет писать письмо при ненулевом коде возврата, но письма теряются в спаме, а полное отсутствие запуска он не отслеживает. Healthchecks решает эту проблему иначе: задача сама отмечается на сервере, а сервер поднимает тревогу, если отметки не было вовремя или пришёл код ошибки.
Модель работы: пинг вместо ожидания
Для каждой проверки создаётся уникальный идентификатор. Задача обязана отправить HTTP-запрос на адрес вида https://hc.example.com/ping/<uuid>. Успешный пинг продлевает ожидание, отсутствие пинга дольше периода плюс льготного интервала даёт статус «просрочено», а пинг с ненулевым кодом — статус «упало». Логика обратна обычному мониторингу: сервер не опрашивает вашу машину, поэтому достаточно исходящего HTTPS и не нужен открытый порт.
Расписание задаётся либо простым периодом в секундах, либо cron-выражением. Простое расписание не принимает значения меньше минуты, что отсекает случайные опечатки. Льготный интервал добавляет запас: задача на два часа не должна считаться упавшей через минуту после старта.
Свой экземпляр: минимальный compose
Сервис с открытым кодом можно развернуть рядом с бэкапами и не зависеть от внешнего адреса. Ниже конфигурация с PostgreSQL: SQLite подойдёт для десятка проверок, но при параллельных пингах лучше база посерьёзнее.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: hc
POSTGRES_USER: hc
POSTGRES_PASSWORD: change-me
volumes:
- db:/var/lib/postgresql/data
web:
image: healthchecks/healthchecks
env_file: .env
ports:
- "8000:8000"
depends_on:
- db
volumes:
db:
В .env минимум таких значений: SECRET_KEY, DEBUG=0, ALLOWED_HOSTS=hc.example.com, SITE_ROOT=https://hc.example.com, DB=postgres, DB_HOST=db, DB_NAME=hc, DB_USER=hc, DB_PASSWORD. Для писем добавляют EMAIL_URL. Если образ не выполнил миграции сам, их запускают вручную.
docker compose up -d
docker compose run --rm web ./manage.py migrate
docker compose run --rm web ./manage.py createsuperuser
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/
Ответ 200 на главной означает, что приложение поднялось. Дальше обязателен HTTPS: пинги идут с секретным идентификатором в пути, и по открытому каналу его легко подсмотреть. Прокси с сертификатом ставится перед контейнером, а SITE_ROOT должен совпадать с внешним адресом, иначе ссылки в уведомлениях будут вести в никуда.
Обёртка для cron
Правильнее не разбрасывать curl по crontab, а сделать один скрипт: он отправляет сигнал старта, выполняет задачу, сохраняет вывод и передаёт реальный код возврата. Тогда в интерфейсе видно и время работы, и текст ошибки, а не просто факт падения.
#!/bin/sh
set -u
URL="$HC_PING"
OUT="$(mktemp)"
curl -fsS -m 10 --retry 3 "$URL/start" >/dev/null 2>&1
/usr/local/bin/backup.sh >"$OUT" 2>&1
rc=$?
curl -fsS -m 10 --retry 3 --data-binary @"$OUT" "$URL/$rc" >/dev/null 2>&1
rm -f "$OUT"
exit "$rc"
Обратите внимание на --retry 3 и -m 10: сеть может моргнуть, и терять из-за этого отметку об успешном бэкапе обидно. Код возврата передаётся в пути, поэтому 0 означает успех, любое другое число — сбой с сохранённым текстом. Тело запроса ограничено, поэтому болтливый вывод лучше обрезать через tail -c 10000.
В crontab нужны две детали: flock от параллельного запуска и явный PATH, потому что окружение cron почти пустое.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
30 3 * * * flock -n /run/backup.lock /usr/local/bin/backup-ping.sh
flock -n не станет ждать: если предыдущий запуск ещё идёт, второй просто выйдет. Это лучше, чем два rsync одновременно.
Таймер systemd как альтернатива
Если cron заменён таймерами, обёртка та же, меняется только запуск. Юнит с OnCalendar удобнее читать, а RandomizedDelaySec разводит одновременные задачи.
[Unit]
Description=Nightly backup with health check
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-ping.sh
Nice=10
IOSchedulingClass=idle
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Persistent=true догоняет пропущенный запуск после выключенной машины. Это создаёт нюанс: если сервер пролежал сутки, задача стартует сразу после загрузки, и проверка не должна успевать объявить тревогу раньше времени — спасает льготный интервал.
Проверка через API и разбор ложных тревог
Список проверок и их статусы доступны по API с ключом из настроек проекта. Это удобно для собственного дашборда и для быстрой проверки после переезда.
curl -fsS -H "X-Api-Key: $HC_KEY" https://hc.example.com/api/v3/checks/ | jq '.checks[] | {name, status, last_ping}'
curl -fsS -o /dev/null -w "%{http_code} %{time_total}\n" https://hc.example.com/ping/$HC_UUID
journalctl -u backup-ping.service -n 50 --no-pager
Ложные срабатывания почти всегда объясняются тремя причинами: задача идёт дольше периода, сеть до сервера проверок нестабильна, либо пинг отправляется после exit. Первое лечится увеличением периода и льготного интервала, второе — повторами в curl, третье — порядком строк в скрипте. Полезно раз в месяц смотреть на график длительности: постепенный рост предупреждает о распухшей базе или диске задолго до падения.
Что стоит отслеживать кроме бэкапа
- Свежесть репликации и наличие свободного места на целевом хранилище.
- Ротацию журналов: задача, которая не удалась, но и не упала, тоже проблема.
- Обновление сертификатов и срок действия домена.
- Синхронизацию с внешним диском, который кто-то мог отключить.
- Проверку восстановления: тестовое развёртывание из копии раз в квартал.
Чек-лист внедрения
- У каждой критичной задачи свой идентификатор, а не один общий на всё.
- Период и льготный интервал соответствуют реальному времени работы.
- Скрипт отправляет старт, вывод и настоящий код возврата.
- Настроен хотя бы один независимый канал уведомлений.
- Проверено, что при отключённой сети тревога приходит, а при восстановлении снимается.
Итог
Healthchecks закрывает слепую зону cron: он замечает и падение, и молчание. Внедрение сводится к трём шагам — создать проверку, обернуть задачу скриптом с пингом и настроить уведомления. Свой экземпляр в compose даёт независимость от чужого сервиса, но требует HTTPS и внимания к секретному идентификатору. Главное правило простое: пинг должен отправляться в любом исходе, иначе мониторинг будет рассказывать только о хороших днях.