Чем PHP CLI отличается от PHP-FPM для cron-задач
PHP-FPM обслуживает HTTP-запросы через Nginx и работает в режиме пула воркеров с общим php.ini и лимитами, заданными для веб-нагрузки. PHP CLI — это отдельный бинарник /usr/bin/php8.3, который запускается из командной строки или из crontab и читает свой собственный конфиг, обычно из /etc/php/8.3/cli/php.ini. Значения memory_limit, max_execution_time и error_reporting в CLI и FPM могут отличаться, и это частая причина, почему скрипт в браузере работает, а тот же скрипт по расписанию падает или наоборот.
Второе отличие — окружение. У процесса cron нет переменных PATH, HOME и локали, которые есть у интерактивной оболочки, и нет рабочего каталога по умолчанию: он всегда стартует из домашнего каталога пользователя, от которого запущен cron.
Как правильно прописать задачу в crontab
Правило crontab должно указывать полный путь к бинарнику PHP, полный путь к скрипту и рабочий каталог, если скрипт использует относительные пути к файлам.
*/5 * * * * cd /var/www/app && /usr/bin/php8.3 artisan schedule:run >> /var/log/app/cron.log 2>&1
Символ && гарантирует, что php запустится только после успешного перехода в каталог, а 2>&1 перенаправляет ошибки в тот же лог-файл, что и обычный вывод. Без этого фатальные ошибки PHP молча исчезают: cron отправляет их на локальную почту, которую никто не читает.
Своя версия php.ini для CLI
Проверить, какой конфиг реально использует CLI, можно одной командой — она покажет путь к файлу, а не предположение.
php --ini
php -i | grep memory_limit
Если на сервере установлено несколько версий PHP, как описано в статье о нескольких версиях PHP на одном сервере, в crontab обязательно указывайте версию явно: /usr/bin/php8.3, а не просто php — иначе задача может внезапно выполниться под другой версией после обновления alternatives.
Память, таймауты и рабочий каталог: частые грабли
| Параметр | Веб (FPM) | Cron (CLI) |
|---|---|---|
| memory_limit | 128M | часто без ограничения (-1) |
| max_execution_time | 30 секунд | 0, ограничения нет |
| рабочий каталог | корень сайта | домашний каталог пользователя |
| error_log | лог PHP-FPM | stderr, если не задан явно |
Значения из таблицы задаются в разных секциях php.ini: [PHP] влияет на оба режима, а блок cli/conf.d — только на CLI. Управление памятью и загрузкой подробно разобрано в статье про memory_limit и upload_max_filesize.
Логирование вывода cron-задач
- Всегда перенаправляйте stdout и stderr в отдельный файл лога, а не полагайтесь на почту cron.
- Указывайте display_errors = Off и log_errors = On в cli/php.ini — вывод ошибок в консоль ломает формат, если скрипт печатает JSON или CSV.
- Ротацию log-файла cron настраивайте через logrotate, иначе файл вырастет до гигабайтов за месяц.
- Разбор упавших задач начинайте с error_log, а не с догадок — принципы разобраны в статье про логи PHP и разбор фаталов.
- Добавляйте в начало каждой задачи date и hostname в лог — на кластере из нескольких серверов иначе не понять, кто именно писал в лог.
Итог: чек-лист для PHP-задач в cron
- В crontab указан полный путь к бинарнику нужной версии PHP и рабочий каталог через cd.
- Вывод команды перенаправлен в файл лога с ротацией через logrotate.
- Проверены memory_limit и max_execution_time именно в cli/php.ini, а не в конфиге FPM.
- display_errors выключен, log_errors включён, ошибки идут в файл, а не в почту root.
- Задача проверена вручную от имени того же пользователя, от которого её запускает cron.
Частые вопросы
Что будет, если в crontab не указать полный путь к бинарнику PHP?
Если указать просто php без полного пути, задача может выполниться под другой версией или вовсе не запуститься: у процесса cron нет переменной PATH интерактивной оболочки, и бинарник ищется совсем не там, где его находит обычный терминал пользователя. Поэтому в crontab всегда прописывают полный путь к бинарнику нужной версии, а не короткое имя команды.
Можно ли увеличить memory_limit только для одной cron-задачи, не трогая остальные?
Да, лимиты памяти и времени выполнения можно задать прямо в вызове, не трогая общий php.ini: ключи -d memory_limit=... и -d max_execution_time=... передаются php как аргументы командной строки и действуют только для этого конкретного запуска. Такой подход удобнее правки общего конфига, если тяжёлая задача всего одна, а остальные скрипты должны работать с лимитами по умолчанию.
Почему cron-задача, которая прекрасно работает вручную из терминала, падает по расписанию?
Обычно дело в разном окружении, а не в самом коде скрипта: в интерактивной оболочке заданы PATH, HOME и локаль, подключён привычный php.ini CLI с нужными настройками, а рабочий каталог — тот, из которого скрипт запустили руками. Процесс cron стартует без этого окружения и из домашнего каталога пользователя, поэтому относительные пути к файлам и переменные окружения нужно прописывать явно.
Обязательно ли перенаправлять вывод cron-задачи в файл, если ошибок обычно не бывает?
Да, перенаправлять вывод стоит всегда, даже если задача обычно отрабатывает без ошибок. Без явного редиректа в файл фатальные ошибки PHP уходят в локальную почту cron, которую на сервере почти никто не читает, и первый же сбой останется незамеченным до жалобы пользователей. Лог с ротацией через logrotate — единственный надёжный способ увидеть, что именно пошло не так.