Фоновые задачи на сервере — бэкапы, очистка сессий, импорт данных, рассылки — почти всегда крутятся через cron. На свежем VDS с только что настроенными задачами всё работает бодро, но со временем появляются знакомые симптомы:
- отчёты не приходят вовремя;
- задачи "залипают" и висят сутками;
- скрипт запускается в двух экземплярах и бодается за lock-файл или базу данных;
- ошибка давно в логах, но вы узнаёте о ней через неделю.
Задача этой статьи — навести порядок: настроить healthchecks для cron-задач на VDS, выстроить понятную схему логирования, добавить блокировки (lock), лимиты времени и оповещения. Всё — без тяжёлого оркестратора, на чистом Linux.
Какие проблемы мы решаем healthchecks для cron
Под healthcheck'ами для cron на VDS будем понимать не только внешние HTTP-пинги, но и внутренние проверки:
- запустилась ли задача;
- завершилась ли она за разумное время;
- не запущена ли в нескольких экземплярах;
- вернула ли нулевой код выхода;
- есть ли свежие логи выполнения;
- пришла ли метрика или отчёт в систему мониторинга.
Самые неприятные сбои — тихие: cron молчит, скрипт молчит, всё выглядит зелёным, пока вы не заметите дыру в бэкапах или неразосланные письма.
Поэтому цель — сделать так, чтобы любой сбой cron-задачи либо быстро сам устранялся (рестарт, принудительный kill зависшего процесса), либо мгновенно подсвечивался в мониторинге.
Базовая гигиена cron на VDS
Прежде чем городить healthchecks, стоит привести в порядок сами задачи cron. Это сильно упростит дальнейший контроль и даст стабильную базу для мониторинга.
Явное логирование и exit-коды
Каждая серьёзная cron-задача должна:
- писать лог (stdout + stderr);
- возвращать осмысленный exit-код (0 — успех, >0 — ошибка);
- по возможности — писать короткую итоговую строку со статусом.
Минимальный шаблон обёртки под любую задачу:
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="/var/log/myjobs"
JOB_NAME="daily-report"
LOG_FILE="${LOG_DIR}/${JOB_NAME}.log"
mkdir -p "${LOG_DIR}"
{
echo "==== $(date -Is) START ${JOB_NAME} ===="
# ВАША ЛОГИКА ТУТ
php /srv/project/artisan reports:daily
echo "==== $(date -Is) DONE ${JOB_NAME} (OK) ===="
} >>"${LOG_FILE}" 2>&1
Такой подход сразу даёт точку опоры для healthcheck-скрипта: можно проверить свежесть и содержимое логов, а также факт успешного завершения.
Отдельный пользователь для cron-задач
Частая ошибка — вешать тяжёлые cron'ы на root. Лучше создать отдельного системного пользователя, даже если это обычный VDS с одним проектом:
useradd --system --home-dir /srv/project --shell /usr/sbin/nologin cronjobs
Дальше редактировать его crontab:
crontab -u cronjobs -e
Это уменьшит потенциальный вред от багов в скриптах и упростит аудит: все задачи видны в одном месте, права в файловой системе ограничены.
Если вы только планируете инфраструктуру и распределяете фоновые задачи по нескольким окружениям, имеет смысл сразу разделить их по нескольким пользователям и VDS с понятной зоной ответственности. Для части задач подойдёт и виртуальный хостинг с доступом к cron, если нагрузка невысокая и нет требований к экзотическим пакетам.

Lock-механизмы для cron: защита от дабл-старта
Любой healthcheck теряет смысл, если одна и та же задача может стартовать в нескольких копиях и мутузить одни и те же данные. Для VDS с несколькими активными cron'ами и слабым диском это особенно критично.
Здесь нас интересует два вида блокировок:
- файловый lock (системный
flock); - lock-файл с PID и контролем "зависших" процессов.
Простой и надёжный вариант: утилита flock
Практически во всех современных дистрибутивах есть flock, и для cron это почти идеальный инструмент.
Вариант 1: оборачиваем команду прямо в crontab:
*/5 * * * * flock -n /run/cron.daily-report.lock php /srv/project/artisan reports:daily
Опции:
-n— не ждать, если lock уже занят; задача просто не стартует;- файл lock лежит в
/run(tmpfs), чтобы он точно не "застрял" после ребута.
Если нужна возможность один раз дождаться освобождения блокировки, но не бесконечно:
*/5 * * * * flock -w 300 /run/cron.daily-report.lock php /srv/project/artisan reports:daily
Здесь -w 300 означает: ждать до 300 секунд, затем выйти с ошибкой, если lock не освобождён. Healthcheck потом может заметить такую ошибку по логам или exit-коду.
Lock-файл с PID и таймаутом
Иногда важно уметь:
- однозначно определить, какой процесс сейчас владеет lock'ом;
- отлавливать "висящие" процессы, которые держат lock слишком долго, и, например, их убивать.
Пример простой реализации на bash:
#!/usr/bin/env bash
set -euo pipefail
LOCK_FILE="/run/daily-report.pid"
MAX_AGE=3600
if [ -f "${LOCK_FILE}" ]; then
PID=$(cut -d ' ' -f1 "${LOCK_FILE}")
START_TS=$(cut -d ' ' -f2 "${LOCK_FILE}")
NOW=$(date +%s)
if ps -p "${PID}" >/dev/null 2>&1; then
AGE=$((NOW - START_TS))
if [ "${AGE}" -gt "${MAX_AGE}" ]; then
echo "Process ${PID} is stuck for ${AGE}s, killing" >&2
kill "${PID}" || true
else
echo "Job already running with PID=${PID}, exiting" >&2
exit 0
fi
else
echo "Stale lock found (PID ${PID} not running), removing" >&2
fi
rm -f "${LOCK_FILE}"
fi
echo "$$ $(date +%s)" >"${LOCK_FILE}"
trap 'rm -f "${LOCK_FILE}"' EXIT INT TERM
# ВАША ЛОГИКА ВЫПОЛНЕНИЯ ЗАДАЧИ
php /srv/project/artisan reports:daily
Этот шаблон полезен, когда одна задача может работать долго (например, часами), но вы хотите отлавливать зависания и не допускать параллельных запусков.
Если вы уже используете systemd timers вместо части cron-задач, посмотрите материал об автоматизации задач через systemd timers и их преимущества перед голым cron: там есть встроенные механизмы ограничения времени и рестартов.
Подходы к healthchecks для cron
После того как мы навели базовый порядок с lock'ами и логами, можно переходить к собственно healthchecks. На VDS удобно использовать несколько уровней контроля:
- микро-проверки внутри самих скриптов;
- локальный watchdog-скрипт, который бегает по задачам и проверяет свежесть логов и lock'ов;
- интеграция с системой мониторинга (Prometheus, Zabbix и т.п.);
- иногда — внешний HTTP healthcheck (например, self-hosted Uptime Kuma или SaaS-сервис).
Встроенный healthcheck в саму задачу
Минимальный набор, который стоит добавить внутрь каждого важного скрипта:
- явное логирование начала и конца;
- учёт времени выполнения (start/finish timestamp);
- запись статуса в отдельный state-файл.
Пример state-файла:
/var/lib/cron-health/daily-report.state
Содержимое:
status=ok
started_at=2025-11-21T03:00:01+00:00
finished_at=2025-11-21T03:00:05+00:00
duration_sec=4
message=Report generated, 253 rows
В конце задачи просто перезаписываем этот файл. Healthcheck-скрипт потом может по нему понять, когда задача последний раз успешно закончилась и сколько она заняла времени.
Локальный watchdog по cron-задачам
Обычно достаточно завести один "мастер"-healthcheck, который раз в несколько минут проверяет все важные cron-джобы по их state-файлам и lock'ам. Такой watchdog удобно реализовать на bash или Python.
Что он должен уметь:
- знать перечень задач и их ожидаемую периодичность (каждые 5 минут, раз в час и т.д.);
- проверять свежесть
finished_atи статус; - при необходимости — проверять отсутствие застарелых lock'ов и висящих процессов;
- в случае проблемы — писать в syslog, отправлять письмо или триггерить webhook в мониторинг.
Минимальная конфигурация может выглядеть как простой INI- или YAML-файл:
[daily_report]
state_file = /var/lib/cron-health/daily-report.state
interval_sec = 3600
max_delay_sec = 600
[fast_job]
state_file = /var/lib/cron-health/fast-job.state
interval_sec = 300
max_delay_sec = 180
Логика проверки для каждой задачи:
- Прочитать
finished_atиstatus. - Проверить, что
status=ok. - Проверить, что
now - finished_at <= interval_sec + max_delay_sec. - Если условие не выполняется — фиксировать инцидент.
Такой watchdog спокойно живёт на том же VDS, где крутятся cron'ы, и не требует внешней инфраструктуры. Для небольших проектов на одном сервере с виртуальным хостингом подобный подход тоже можно использовать, если есть доступ к crontab и логам пользователя.
Healthchecks через логи: свежесть и ошибки
Если не хочется внедрять отдельные state-файлы, можно использовать логи как источник правды. Для этого нужно соблюдать пару правил:
- одна задача — один лог-файл (не смешивать разные задачи в один);
- каждый запуск должен писать уникальный маркер начала и конца;
- при успехе — явный маркер (например,
DONE (OK)); - при ошибке — ненулевой exit-код и запись в лог.
Пример простого healthcheck-скрипта на bash для проверки по логам:
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="/var/log/myjobs/daily-report.log"
MAX_AGE_MIN=70 # задача раз в час, берём с запасом
if [ ! -f "${LOG_FILE}" ]; then
echo "CRIT: log file ${LOG_FILE} not found"
exit 2
fi
LAST_TS=$(stat -c %Y "${LOG_FILE}")
NOW=$(date +%s)
AGE_MIN=$(( (NOW - LAST_TS) / 60 ))
if [ "${AGE_MIN}" -gt "${MAX_AGE_MIN}" ]; then
echo "CRIT: log file ${LOG_FILE} too old (${AGE_MIN} min)"
exit 2
fi
if ! grep -q "DONE (OK)" "${LOG_FILE}"; then
echo "WARN: no DONE (OK) marker found in ${LOG_FILE}"
exit 1
fi
echo "OK: daily-report looks healthy"
exit 0
Такой скрипт можно:
- дёргать самим cron'ом (и логировать его вывод);
- подключить к Prometheus Node Exporter (textfile-collector);
- подключить к Zabbix как user-parameter.

Интеграция cron healthchecks с мониторингом на VDS
У большинства продакшн-проектов на VDS уже есть какая-то система мониторинга. Задача — встроить туда статус cron-задач, чтобы алерты по ним были наравне с алертами по диску, памяти и доступности.
Prometheus: textfile-collector и метрики задач
Для VDS с Node Exporter один из самых удобных вариантов — экспортировать по файлу метрик на диск, а Node Exporter сам их подберёт через textfile-collector.
Файл, например:
/var/lib/node_exporter/cron_health.prom
Содержимое:
cron_job_status{job="daily_report"} 0
cron_job_status{job="fast_job"} 1
cron_job_last_success_unixtime{job="daily_report"} 1732167605
Где:
cron_job_status: 0 — ок, 1 — warning, 2 — критическая ошибка;cron_job_last_success_unixtime— timestamp последнего успешного запуска.
Ваш watchdog-скрипт просто периодически перезаписывает этот файл на основе state-файлов или логов, а Prometheus и Alertmanager уже поднимают алерты при нарушениях (например, если разница между текущим временем и cron_job_last_success_unixtime вышла за пределы).
Zabbix: userparameter и логика на стороне агента
Для Zabbix путь похожий: задача watchdog-скрипта — вернуть короткий текстовый статус или числовой код, который Zabbix-агент отдаст в качестве item'а. Остальное делаете триггерами.
Пример userparameter:
UserParameter=cron.health.daily_report,/usr/local/bin/cron-health daily_report
Здесь cron-health — ваш скрипт, который возвращает 0, 1 или 2 и короткое сообщение. В триггерах можно уже интерпретировать коды, строить графики задержек и т.п.
Контроль времени выполнения и таймауты
На уровне cron и VDS полезно ограничивать максимальное время выполнения задач, чтобы они не зависали навсегда и не забивали ресурсы. Подходов несколько.
GNU timeout: быстрый и простой вариант
Большинство дистрибутивов содержит утилиту timeout из GNU coreutils. Её легко добавить прямо в crontab:
0 3 * * * timeout 1800 /usr/local/bin/daily-report.sh
Здесь задача будет принудительно убита через 1800 секунд (30 минут), если не завершится раньше. В exit-коде можно распознать причину:
- 124 — сработал timeout;
- >128 — задача убита сигналом;
- 0 — успех;
- любое другое значение >0 — ошибка самой задачи.
Healthcheck-скрипт может анализировать эти коды по логам и заводить инциденты, если таймаут срабатывает слишком часто.
systemd units и timers вместо cron
Если на вашем VDS задачи критичны и вы хотите более гибких возможностей (таймауты, рестарты, логирование через journald, интеграция с watchdog'ом systemd), имеет смысл перенести часть cron'ов на systemd timers. Но это уже отдельная большая тема; здесь лишь зафиксируем, что для сложных сценариев systemd даёт:
TimeoutStartSecиRuntimeMaxSecна уровне юнита;- рестарты по не-нулевому exit-коду;
- механизмы
WatchdogSecиsystemd-notifyдля продвинутых healthchecks.
Подробный разбор можно посмотреть в статье о переходе с cron на systemd timers и типовых сценариях миграции. Если у вас уже есть systemd timers, логично использовать и systemd-watchdog для контроля долгоживущих задач.
Внешние healthchecks для cron по HTTP
Отдельный полезный класс решений — внешние healthcheck-сервисы: SaaS или self-hosted, которые принимают HTTP-пинги и следят, что задача отметилась в заданный интервал.
На уровне cron это выглядит так:
- создаёте задачу в сервисе, получаете уникальный URL;
- в начале и в конце скрипта добавляете вызов утилиты для HTTP-запроса к этому URL;
- если пинг не приходит вовремя — сервис присылает уведомление по почте или в мессенджер.
Пример обёртки (URL показаны условно, без рабочих адресов):
#!/usr/bin/env bash
set -euo pipefail
HC_START="https://example.local/hc/uuid/start"
HC_END="https://example.local/hc/uuid/end"
curl -fsS "${HC_START}" || true
if php /srv/project/artisan reports:daily; then
curl -fsS "${HC_END}" || true
else
curl -fsS "${HC_END}?status=fail" || true
exit 1
fi
Преимущества внешнего healthcheck'а:
- вы получаете уведомление даже если VDS "лежит" целиком (timeout по пингу);
- не нужно поднимать собственный мониторинг ради пары критичных задач;
- часто есть удобная визуализация истории запусков.
Недостаток — дополнительная внешняя зависимость. На продакшн-кластерах обычно комбинируют: внутренний мониторинг по state-файлам и метрикам плюс внешний watchdog по HTTP для нескольких самых важных задач.
Практический чек-лист по cron healthchecks на VDS
Подведём всё в виде списка шагов, с которыми можно пройтись по своему VDS и навести порядок.
- Инвентаризация cron-задач. Соберите все crontab'ы (системный, пользовательские, каталог
/etc/cron.d) в один документ, сверьте расписания и критичность. - Отдельный пользователь для задач. Вынесите всё не-системное в отдельного пользователя
cronjobsили по одному пользователю на проект. - Стандартизируйте логирование. Каждая задача пишет в свой лог-файл, в начале и в конце — маркеры и timestamp.
- Добавьте lock. Для любой задачи, которая может физически не успевать завершиться к следующему запуску, обязательно включите
flockили PID-lock с таймаутом. - Ограничьте время выполнения. Для долгих скриптов используйте
timeoutили переведите их наsystemd timersс параметромRuntimeMaxSec. - Внедрите state-файлы или стабильный формат логов. Это позволит healthcheck-скриптам легко анализировать состояние.
- Напишите watchdog-скрипт. Один скрипт на VDS, который проходит по задачам, проверяет свежесть, статус, lock'и и отдаёт результат в мониторинг или на почту.
- Интегрируйте с мониторингом. Если используете Prometheus — textfile-collector; если Zabbix — userparameter; другие системы мониторинга — через их стандартные механизмы.
- Подумайте о внешних HTTP healthchecks. Для самых важных задач добавьте HTTP-пинги в сторонний сервис или self-hosted решение.
- Документируйте и пересматривайте. Опишите схему в README или wiki: какие задачи есть, где их логи, какие healthchecks и оповещения. Раз в квартал пересматривайте конфигурацию.
В результате cron на вашем VDS из "чёрного ящика" превратится в предсказуемую систему с понятными гарантиями: вы будете знать, какая задача, когда и как отработала, а любые отклонения будут быстро подсвечиваться мониторингом.


