Перейти к содержимому
Почта Личный кабинет Выбрать тариф

Cron на хостинге: как настроить задачи и исправить типичные ошибки

Cron на хостинге: как настроить задачи и исправить типичные ошибки

Cron на хостинге запускает команды и скрипты по расписанию от имени вашего пользователя: задачу добавляют в панели управления (обычно это раздел cron) или командой crontab -e по SSH. Когда задача «не работает», дело редко в самом расписании. Обычно мешает другое окружение: у cron нет вашего PATH, рабочий каталог другой, знак % обрезает команду, а вывод уходит письмами. Ниже восемь таких ошибок, которые мы воспроизвели на тестовом стенде, с реальным выводом и исправлениями, а также замеры WP-Cron против системного cron.

Как мы проверяли

10 ноября 2026 года на тестовом стенде: контейнер Ubuntu 24.04.5, пакет cron 3.0pl1-184ubuntu2 (стандартный cron Debian и Ubuntu), отдельный пользователь без прав root, PHP 8.3.6 в двух режимах (CLI и PHP-FPM с memory_limit 128M), nginx 1.24, MariaDB 10.11, WordPress 6.4.3. Каждую задачу ставили на запуск ежеминутно и проверяли журнал cron, письма, которые он отправляет, и собственные журналы задач. Таймеры systemd проверили только утилитой systemd-analyze, потому что в контейнере systemd не запущен. Пути в примерах относятся к стенду. Это поведение тестового стенда, а не данные с серверов RichHost.

Как читать строку crontab

Строка состоит из пяти полей времени и команды. Время считается по часам сервера, а не по вашим (почему это важно, видно в ошибке №6).

# мин  час  день мес  день_недели  команда
*/5    *    *    *    *            команда   # каждые 5 минут
15     3    *    *    *            команда   # ежедневно в 03:15
0      9    *    *    1-5          команда   # в 09:00 по будням
30     2    1    *    *            команда   # 1 числа каждого месяца в 02:30
@daily                             команда   # раз в сутки в 00:00

Одна ловушка есть уже в синтаксисе: если заполнить и день месяца, и день недели (0 3 1 * 1), задача сработает и первого числа, и каждый понедельник, а не только в понедельник, выпавший на первое.

Как добавить задачу на хостинге

В панели управления

В панели управления хостингом откройте раздел cron: там обычно есть кнопка добавления, поле для команды и пять полей расписания. Задача выполняется от имени вашего пользователя, поэтому sudo в ней недоступен. Указывайте полный путь к интерпретатору, например /usr/bin/php8.2: версия PHP для cron может отличаться от выбранной для сайта, так что сначала узнайте, какую версию PHP использует сайт. На тарифах RichHost сайтами управляют через собственную панель управления с ИИ-помощником, а SSH-доступ есть на всех тарифах.

По SSH

Если вы уже умеете подключиться по SSH, расписание редактируют командой crontab -e, просматривают через crontab -l. Осторожно с crontab -r: он без вопросов удаляет всё расписание. Надёжная строка задачи выглядит так:

MAILTO=""
*/10 * * * * cd /home/user/web/example.com/public_html && flock -n /home/user/tmp/export.lock /usr/bin/php8.3 cron/export.php >> /home/user/logs/export.log 2>&1

Здесь закрыты сразу пять ловушек из следующего раздела: рабочий каталог, полный путь к PHP, защита от наложения, журнал вместо писем и ни одного знака % в команде.

Восемь ошибок, которые мы воспроизвели

1. По SSH команда работает, а в cron пишет «not found»

Cron даёт задаче минимальное окружение. Вот что задача записала в файл командой env:

HOME=/srv/cw6/home/cw6user
LOGNAME=cw6user
PATH=/usr/bin:/bin
SHELL=/bin/sh
PWD=/srv/cw6/home/cw6user

В SSH-сессии того же пользователя composer лежал в /usr/local/bin, которого в этом PATH нет. Результат в журнале задачи и код выхода 127 в журнале cron:

/bin/sh: 1: composer: not found
(CRON) error (grandchild #19489 failed with exit status 127)

Исправление: полный путь (/usr/local/bin/composer, узнать его можно командой which composer по SSH) или строка PATH=... в начале crontab. Это касается также node, wp и конкретных версий php. Обратите внимание и на SHELL=/bin/sh: синтаксис bash вроде [[ ]] или source лучше вынести в отдельный скрипт с первой строкой #!/bin/bash.

2. Относительные пути: скрипт не видит свой конфиг

Скрипт со строкой require './config.php'; прекрасно работал из каталога сайта. В cron рабочим каталогом служит домашний каталог, поэтому тот же скрипт падал каждую минуту:

PHP Warning:  require(./config.php): Failed to open stream: No such file or directory in .../site/task.php on line 2
PHP Fatal error:  Uncaught Error: Failed opening required './config.php' (include_path='.:/usr/share/php')

Исправление: cd /путь/к/сайту && php task.php (на стенде после этого задача отрабатывала каждую минуту) или пути в самом скрипте от __DIR__.

3. Знак % обрезает команду

В crontab всё после первого неэкранированного % передаётся команде как стандартный ввод. Строка echo "run $(date +%Y-%m-%d)" >> e3.log 2>&1 вообще не создала журнал, потому что перенаправление оказалось после %, а пользователь получил письмо:

Subject: Cron <cw6user@vm> echo "run $(date + (failed)
/bin/sh: 1: Syntax error: end of file unexpected (expecting ")")

Исправление: date +\%Y-\%m-\%d или перенос команды в скрипт, где % экранировать не нужно.

4. Задача молча проигнорирована из-за формата файла

Команда crontab проверяет файл перед установкой и не приняла файл без символа перевода строки в конце (new crontab file is missing newline before EOF, can't install.), а также пустую переменную MAILTO= без кавычек (bad minute; правильно MAILTO=""). Гораздо коварнее на VPS, где файлы кладут прямо в /etc/cron.d. На стенде из четырёх файлов сработал только один:

  • без перевода строки в конце: ERROR (Missing newline before EOF, this crontab file will be ignored);
  • с точкой в имени (backup.cron): проигнорирован без единой записи в журнале;
  • с правами 664: INSECURE MODE (group/other writable) каждую минуту, задача не запускалась;
  • backup-ok с правами 644 и переводом строки: работал.

5. Письмо на каждый запуск вместо журнала

Если команда что-то выводит и вывод никуда не перенаправлен, cron отправляет его письмом владельцу. Две такие задачи каждую минуту дали два письма в минуту, то есть 120 в час, с темой вроде Cron <cw6user@vm> php -r 'echo "processed 42 orders\n";'. Когда почтового сервера нет, cron пишет No MTA installed, discarding output и вывод просто пропадает вместе с сообщениями об ошибках.

Исправление: >> /путь/job.log 2>&1 для журнала или MAILTO="", если вывод не нужен. Компромиссный вариант: >> job.log без 2>&1, тогда обычный вывод идёт в журнал, а письмо приходит только тогда, когда скрипт пишет ошибку.

6. Часовой пояс: CRON_TZ на Ubuntu не работает

Сервер считает расписание в своём часовом поясе, часто это UTC. Мы поставили CRON_TZ=Asia/Kathmandu и время 19:31 по Катманду (13:46 UTC): задача не запустилась ни в 13:46, ни позже. Справка crontab(5) этого пакета прямо говорит, что отдельные часовые пояса для пользователей не поддерживаются. Переменная TZ=Europe/Kyiv в crontab меняет только вывод date внутри задачи (date says 15:44:01 EET, utc 13:44:01), а не момент запуска. Ещё одна находка: старое название Europe/Kiev на Ubuntu 24.04 без пакета tzdata-legacy молча даёт UTC.

Исправление: пересчитайте время в пояс сервера (03:15 по Киеву зимой это 01:15 UTC, летом 00:15) или запускайте задачу ежечасно и проверяйте час в самой команде: [ "$(TZ=Europe/Kyiv date +\%H)" = 03 ] && .... В cronie (AlmaLinux, Rocky Linux) CRON_TZ поддерживается, в таймерах systemd пояс пишут прямо в расписании.

7. Следующий запуск стартует, пока предыдущий ещё работает

Скрипт, который работает 150 секунд, мы запускали каждую минуту. В 13:46 в системе одновременно жили три его копии (13:44, 13:45 и 13:46). Для резервного копирования или импорта товаров это значит двойные записи и конкуренцию за диск. С flock -n вторая копия просто не стартует:

13:50:01 start pid=22979
13:51:02 skipped: previous run still holds the lock
13:52:01 skipped: previous run still holds the lock
13:52:31 end   pid=22979
13:53:01 start pid=3603

Ловушка, в которую мы попали сами: файл блокировки лежал в каталоге, куда пользователь не мог писать. flock завершался с кодом 66 (cannot open lock file ... Permission denied), а конструкция || echo skipped каждую минуту писала «skipped», хотя скрипт не запустился ни разу. Держите файл блокировки в своём домашнем каталоге.

8. PHP в cron не тот, что на сайте

Cron запускает PHP в режиме CLI, а сайт обслуживает PHP-FPM. Это разные файлы настроек и разные ограничения:

Параметр Cron (php-cli) Сайт (PHP-FPM)
php.ini /etc/php/8.3/cli/php.ini /etc/php/8.3/fpm/php.ini
memory_limit -1 (без ограничения) 128M
max_execution_time 0 (без ограничения) 30 секунд

Скрипт, который занимает 200 МБ, в cron отработал (allocated 200 MB under cli), а тот же файл, открытый по URL, дал HTTP 500 и Allowed memory size of 134217728 bytes exhausted, то есть классическую ошибку Allowed memory size. Поэтому задача, которую вызывают через wget по адресу, живёт по правилам сайта, а не cron. На виртуальном хостинге тяжёлые задачи упираются ещё и в лимиты аккаунта, а их превышение может закончиться ошибкой 508.

WP-Cron против системного cron: что показал стенд

WP-Cron не отдельная служба. При загрузке страницы WordPress проверяет список задач и, если что-то пора выполнить, отправляет сам себе фоновый запрос к wp-cron.php. На стенде мы создали событие каждые 5 минут и запланированную запись, а потом не заходили на сайт:

  • событие, запланированное на 13:44:32, выполнилось в 13:46:27, когда страницу загрузили впервые (на 115 секунд позже);
  • за 8,5 минуты без посетителей запланированная запись так и осталась в статусе «запланировано», а следующее событие ждало;
  • первый запрос в 13:55:10 запустил wp-cron.php: событие выполнено с опозданием 338 секунд, запись опубликована через 518 секунд после запланированного времени, а пропущенный запуск в 13:54:32 отдельно не повторялся;
  • самого посетителя фоновый запуск не замедлил: TTFB 0,074 с против 0,066-0,079 с без него, потому что wp-cron.php сразу отдаёт ответ и продолжает работу в отдельном процессе PHP.

Потом отключили WP-Cron и поставили системный cron каждые 5 минут:

// wp-config.php, выше строки "That's all, stop editing!"
define( 'DISABLE_WP_CRON', true );
*/5 * * * * cd /home/user/web/example.com/public_html && /usr/bin/php wp-cron.php >/dev/null 2>&1

После этого три посещения подряд уже не вызывали wp-cron.php, а новая запланированная запись вышла в 14:00:02, на ближайшем запуске cron, через 211 секунд после своего времени. Теперь опоздание не зависит от посетителей и не превышает интервала cron. Другие настройки скорости собраны в материале о том, как ускорить WordPress.

Вариант через адрес (wget -q -O /dev/null "https://example.com/wp-cron.php?doing_wp_cron") тоже рабочий, но выполняется с ограничениями PHP-FPM из пункта 8 и может наткнуться на защиту от ботов. Если файлы сайта в том же аккаунте, надёжнее вызов через php.

Таймеры systemd на своём VPS

На виртуальном хостинге таймеров systemd нет, потому что для них нужен root. На собственном сервере они удобнее cron в трёх вещах: Persistent=true запускает задачу, пропущенную во время выключения сервера, часовой пояс пишут прямо в расписании, а вывод попадает в журнал, а не в почту.

# /etc/systemd/system/site-backup.service
[Unit]
Description=Nightly site backup

[Service]
Type=oneshot
User=user
WorkingDirectory=/home/user/site
ExecStart=/usr/bin/php /home/user/site/cron/backup.php

# /etc/systemd/system/site-backup.timer
[Unit]
Description=Run site-backup daily at 03:15 Kyiv time

[Timer]
OnCalendar=*-*-* 03:15:00 Europe/Kyiv
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

Перед включением проверьте расписание: systemd-analyze calendar "*-*-* 03:15:00 Europe/Kyiv" на стенде показал следующий запуск в 01:15 UTC, а systemd-analyze verify на ошибочном 25:15:00 ответил Failed to parse calendar specification и Timer unit lacks value setting. Refusing. Дальше systemctl enable --now site-backup.timer, список таймеров в systemctl list-timers, вывод в journalctl -u site-backup.service. Как подготовить сам сервер, описано в чек-листе настройки VPS на Ubuntu 24.04.

Симптом, причина, исправление

Симптом Причина Исправление
«not found», код выхода 127 В cron PATH только /usr/bin:/bin Полный путь к программе или PATH= в crontab
«Failed to open stream», хотя по SSH всё работает Рабочий каталог cron это домашний каталог cd /путь && перед командой или __DIR__
Журнал не создаётся, письмо «Syntax error» Неэкранированный % в команде \% или отдельный скрипт
Задача из /etc/cron.d не запускается Нет перевода строки, точка в имени, права 664 Имя без точки, права 644, перевод строки в конце
Ящик забит письмами от Cron Daemon Вывод не перенаправлен >> job.log 2>&1 или MAILTO=""
Задача срабатывает на 2-3 часа раньше или позже Сервер в UTC, CRON_TZ игнорируется Пересчитать время или проверять час в команде
Двойные записи, несколько копий в списке процессов Новый запуск до завершения предыдущего flock -n с файлом блокировки в своём каталоге
В cron работает, по URL ошибка 500 Разные php.ini для CLI и FPM Вызывать через php, поднять лимит для конкретного скрипта
Отметка Missed schedule в WordPress WP-Cron ждёт посетителя DISABLE_WP_CRON и системный cron каждые 5 минут

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

Как часто можно запускать cron?

Минимальный шаг в crontab одна минута. Для WordPress и большинства сайтов достаточно 5-15 минут, для резервных копий и отчётов раз в сутки ночью. Более частый запуск имеет смысл только для очередей, и тогда обязательно с flock.

Как убедиться, что задача действительно выполняется?

Добавьте в команду запись времени в журнал, например date >> job.log перед основной командой, и через несколько минут посмотрите файл. Пустой журнал означает, что задача не стартовала, а строка с ошибкой подскажет, какая из восьми ловушек сработала.

Что лучше: php script.php или wget по адресу?

Если скрипт лежит в вашем аккаунте, лучше вызов через php: нет ограничения 30 секунд, запрос не проходит через веб-сервер и защиту от ботов. Адрес нужен, когда скрипт на другом сервере или плагин требует именно HTTP-запроса.

Выполнит ли cron задачу, пропущенную во время перезагрузки сервера?

Нет. Cron не догоняет пропущенные запуски: если в нужную минуту служба не работала, задача будет ждать следующего раза по расписанию. В таймерах systemd для этого есть Persistent=true.

Еще по теме

Все статьи
SSL-сертификат на хостинге: как установить и перевести сайт на HTTPS
Инструкции и туториалы по хостингу02.08.2026

SSL-сертификат на хостинге: как установить и перевести сайт на HTTPS

Пошаговая инструкция по переходу сайта на HTTPS: типы SSL-сертификатов, установка через панель и Certbot, ловушка смешанного контента и правильные редиректы без потери…

Логи сервера на хостинге: как читать access и error log и быстро найти проблему
Инструкции и туториалы по хостингу02.06.2026

Логи сервера на хостинге: как читать access и error log и быстро найти проблему

Где найти логи сайта на хостинге, чем журнал доступа отличается от журнала ошибок, и готовые команды: топ IP, коды 5xx во времени,…

Swap и OOM Killer: сколько swap нужно серверу и почему убивают MySQL
Настройка серверов08.05.2026

Swap и OOM Killer: сколько swap нужно серверу и почему убивают MySQL

Сколько swap нужно серверу, что означает swappiness и почему OOM Killer убивает MariaDB вместо PHP: тест в cgroup с реальными строками dmesg,…