LabAndor Описать задачу

Данные и парсинг

  • Парсинг сайтов

Импорт и интеграции

Боты и автоматизация

  • Telegram-боты

Сайты и инфраструктура

  • VPS и серверы
  • Парсинг сайтов
  • Telegram-боты
  • VPS и серверы
WordPress и автоматизация

Почему не запускаются cron-задачи и фоновые процессы на VPS

Скрипт работает из консоли, но не запускается по расписанию — самая частая жалоба, с которой к нам приходят на диагностику VPS. Причина почти никогда не в cron как таковом: демон crond у большинства провайдеров работает штатно. Проблема в окружении, в котором задача выполняется без интерактивной сессии. Ниже — по каким местам мы проходим при разборе таких кейсов и что обычно находим.

Cron не выполняется

Первое, что мы проверяем — а вообще ли задача попала в crontab демона. crontab -l показывает список для текущего пользователя, но если задачу добавляли через sudo crontab -e под root, а искать её потом стали в crontab -l от имени обычного пользователя — она просто в другом файле. То же самое с systemd-таймерами: они не отображаются в выводе crontab -l вообще, это отдельный механизм (см. ниже).

Второй частый случай — синтаксис расписания. Пять полей * * * * * (минута, час, день месяца, месяц, день недели) многие путают местами или забывают, что день месяца и день недели соединены логическим ИЛИ, а не И — если оба поля не *, задача выполнится когда сработает любое из условий. Формат и допустимые значения — в man 5 crontab.

Третье — сам crond может не видеть новый файл, если crontab правили не штатной командой crontab, а редактированием файла в /var/spool/cron напрямую. Демон в большинстве реализаций перечитывает spool по времени модификации файла, но правки мимо crontab -e — источник неочевидных багов, надёжнее их избегать.

Пути, окружение и права

Cron запускает задачи в сильно урезанном окружении: своя PATH (часто просто /usr/bin:/bin), нет переменных из .bashrc или .profile, нет текущей рабочей директории пользователя. Скрипт, который в интерактивном шелле находит php, node или python3 по алиасу или через nvm/pyenv, в cron их просто не видит — и падает с «command not found» ещё до того, как дошёл до логики.

Практика, которая закрывает большинство таких случаев:

  • в самой cron-строке или в начале скрипта прописывать полный путь к бинарнику (/usr/bin/php, вывод which php), а не рассчитывать на PATH;
  • явно переходить в рабочую директорию (cd /var/www/project && ...) вместо относительных путей;
  • для задач, которым нужны переменные окружения — прописывать .env-файл и грузить его в самом скрипте, а не надеяться, что cron унаследует пользовательский шелл.

Права — вторая по частоте причина. Скрипт с правами 644 без исполняемого бита не запустится через прямой вызов (./script.sh), только через явный вызов интерпретатора (bash script.sh). Если задача пишет файлы (логи, кэш, экспорт) — обычно всплывает, что cron выполняется от другого пользователя, чем веб-сервер, и упирается в права на директорию.

PHP/Python/Node версии

На VPS с несколькими версиями PHP (типичная ситуация после миграции или при разных проектах на одном сервере) php в PATH может указывать не на ту версию, что использует сайт. Для WordPress и WooCommerce это критично: cron-задача, вызывающая WP-CLI под PHP 8.0, пока сайт работает на 8.1 (или наоборот), может либо тихо завершаться без результата, либо ронять фатальную ошибку несовместимости расширений.

Правило простое: в cron-команде фиксировать версию явно — не php script.php, а /usr/bin/php8.1 script.php, или через update-alternatives проверять, что дефолтный php действительно совпадает с версией сайта. Для Python — то же самое с виртуальным окружением: cron не активирует venv автоматически, путь к интерпретатору должен указывать прямо в venv/bin/python. Для Node — аналогично с nvm: cron не читает .nvmrc, нужен либо абсолютный путь к нужной версии, либо обёртка через nvm exec.

Логи и stderr

По умолчанию cron отправляет вывод задачи (stdout и stderr) на системную почту пользователя — если MTA на сервере не настроен, письмо просто теряется, и задача «падает молча». Первое, что мы добавляем при разборе — явное перенаправление в файл:

* * * * * /usr/bin/php /var/www/project/task.php >> /var/log/project/task.log 2>&1

Без 2>&1 в лог попадёт только stdout, а именно stderr обычно и содержит причину падения — fatal error, unhandled exception, permission denied. Отдельно стоит проверять, не переполнился ли лог: без ротации (logrotate) файл может расти без ограничений и не в лучшую сторону влиять на диск.

Для WordPress отдельный источник правды — wp-content/debug.log, если включён WP_DEBUG_LOG в wp-config.php (подробности параметров отладки — в официальной документации WordPress). Для задач через WP-CLI полезно добавлять --debug на время диагностики, чтобы увидеть полный трейс вместо общей ошибки.

Блокировки и параллельные запуски

Если задача выполняется дольше интервала запуска (например, импорт товаров занимает семь минут при расписании раз в пять), cron честно запускает вторую копию поверх первой. Результат — гонка за одни и те же файлы, дублирование записей в БД, а иногда и блокировка таблиц WooCommerce на время, которое сайт замечает как подвисание оформления заказа.

Стандартное решение — lock-файл или flock:

* * * * * /usr/bin/flock -n /tmp/task.lock /usr/bin/php /var/www/project/task.php >> /var/log/project/task.log 2>&1

flock -n не запустит вторую копию, если предыдущая ещё держит блокировку, а просто выйдет — это дешевле, чем городить самодельную проверку PID-файла, и не требует отдельной очистки после падения процесса.

systemd timers и очереди

На современных дистрибутивах (systemd в качестве init) вместо classic cron всё чаще используют systemd timers — пара файлов .service + .timer. Плюс перед cron: таймер логируется через journalctl -u имя.service, что снимает половину проблем из раздела про логи — не нужно отдельно настраивать перенаправление вывода. Минус — синтаксис OnCalendar= менее привычный, чем пять полей cron, и требует systemctl daemon-reload после правки юнита.

Отдельная категория — фоновые очереди самого приложения, а не системный планировщик. У WordPress это WP-Cron — он не демон, а псевдо-cron, который триггерится реальным посещением сайта (детали механизма — в документации WordPress). На сайте с низким трафиком ночью задачи, завязанные на WP-Cron (запланированные публикации, повторяющиеся email, некоторые хуки WooCommerce), могут откладываться на часы, потому что просто некому дёрнуть страницу. Решение — отключить DISABLE_WP_CRON в wp-config.php и вызывать wp-cron.php системным cron с фиксированным интервалом; это убирает зависимость от случайных визитов.

Мониторинг и уведомления

Задача, которая выполняется беззвучно и падает беззвучно — источник инцидентов, которые вскрываются только когда клиент замечает: «отчёт за вчера не пришёл» или «остатки на сайте не обновились». Минимальный уровень контроля, который мы обычно настраиваем:

  • healthcheck-пинг из скрипта на внешний сервис после успешного завершения — если пинга нет N раз подряд, сервис уведомляет отдельно от самого приложения;
  • код возврата скрипта проверяется, а не только факт запуска — exit 1 при ошибке, чтобы обвязка могла отличить «выполнилось с ошибкой» от «выполнилось успешно»;
  • уведомление в Telegram-бот на критичные фоновые задачи (синхронизация остатков, обработка платежей) — отдельным вызовом при неуспехе, без завязки на почту сервера, которая на VPS часто не настроена вообще.

Чек-лист диагностики

  • Проверили, в чьём crontab лежит задача (crontab -l под правильным пользователем) и не потерялась ли она в /var/spool/cron
  • Сверили синтаксис расписания и логику day-of-month / day-of-week (ИЛИ, не И)
  • В команде указан полный путь к интерпретатору нужной версии, а не голое имя из PATH
  • Скрипт не полагается на переменные окружения интерактивного шелла — либо явный .env, либо абсолютные пути
  • Указан явный cd в рабочую директорию перед выполнением
  • Права на файл и исполняемый бит соответствуют способу вызова
  • Вывод (>> log 2>&1) перенаправлен в файл, а не улетает в несуществующую почту
  • Настроена ротация логов, чтобы файл не рос неограниченно
  • Для задач дольше интервала запуска — есть flock или аналог, исключающий параллельный запуск
  • Для WordPress — WP-Cron либо отключён в пользу системного cron, либо трафика достаточно для его штатной работы
  • Есть внешний сигнал об ошибке (healthcheck, код возврата, уведомление), а не только факт «задача когда-то запускалась»

Большинство пунктов из этого списка закрываются один раз при настройке сервера — дальше это просто работает, без ручного контроля после каждого деплоя. Если фоновые задачи на текущем VPS ведут себя непредсказуемо, разумнее сразу пройтись по конфигурации сервера целиком, а не чинить симптомы по одному: у нас есть отдельная услуга «Настройка VPS-сервера под ключ», которая закрывает окружение, права, cron/systemd и мониторинг за один проход.

Если после чек-листа причина всё ещё не нашлась — пришли вывод crontab -l, содержимое лога задачи и версию ОС, разберём точечно.

Частые вопросы

Как проверить, что cron-задача вообще запускается?

Смотри вывод crontab -l под тем же пользователем, под которым задачу добавляли, и перенаправляй stdout и stderr в отдельный лог-файл — без этого cron молча пишет в системную почту, которая на VPS обычно не настроена. Если лог пустой при каждом запуске — задача либо не попадает в crontab, либо падает раньше первой строчки скрипта.

Почему скрипт работает руками, но не через cron?

Обычно дело в окружении: у cron своя урезанная PATH без алиасов и переменных из .bashrc, поэтому интерпретатор, который находится в интерактивном шелле, в cron просто не виден. Лечится явным полным путём к бинарнику в самой cron-строке, а не переносом логики скрипта.

Cron или systemd timer — что выбрать на новом сервере?

Если дистрибутив на systemd — таймер удобнее: логи сразу идут в journalctl, не нужно отдельно настраивать перенаправление вывода. Классический cron проще для одной-двух задач и привычнее большинству админов — держать смесь того и другого на одном сервере не проблема, если задачи не дублируют друг друга.

Похожие статьи

Разберём вашу задачу

Опишите, что нужно — проверим и вернёмся с предварительной оценкой.

Данные из заявки третьим лицам не передаём.

Опишите задачу

Уточним детали и вернёмся с предварительной оценкой в выбранный канал. Финальные цену и срок подтверждает специалист.

Куда прислать ответ *