Запуск cron-задач Laravel в Docker с FrankenPHP

12.10.2025

Вчера сидел и разбирался с запуском cron-задач в Laravel. Есть там две бесхитростные команды:

php artisan schedule:run
php artisan schedule:work

Подразумевается, что первую мы добавим в настоящий crontab в окружении, где крутится бэкенд, или в любом другом месте, где есть нужные переменные окружения и возможность подключиться к БД.

В crontab должно быть что-то такое:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

Это означает запуск каждую минуту.

Идея в том, чтобы сам график выполнения команд передать под управление PHP — и Laravel в частности. При запуске этой команды скрипт смотрит в app/Console/Kernel.php и проверяет зарегистрированные графики выполнения. Они выглядят примерно так:

$schedule->command('emails:send Taylor --force')->daily();

->daily() в данном случае означает, что команда будет выполняться ежедневно.

Что делает schedule:work?

Вторую команду, согласно документации, нужно запускать только локально. Всё, что она делает, — запускает schedule:run каждую минуту средствами PHP, используя бесконечный цикл.

Первый pull request с реализацией даёт исчерпывающее понимание того, как это работает. В современной версии Laravel код выглядит намного сложнее.

Мой случай: Docker и FrankenPHP

Я запускаю бэкенд-приложение в Docker с помощью Docker Compose. Базовый образ — dunglas/frankenphp.

Вот как сервис объявлен в docker-compose.yml:

backend-cron:
  restart: unless-stopped
  build:
    target: dev
  volumes:
    - .:/app
  command: "frankenphp php-cli artisan schedule:work"
  healthcheck:
    disable: true
  env_file:
    .env

Каждую минуту я получал одну и ту же ошибку:

backend-cron-1  | 2025-12-09T14:54:00.043306279Z sh: 1: : Permission denied
backend-cron-1  | 2025-12-09T14:55:00.041940906Z sh: 1: : Permission denied

По старой памяти я ожидал, что скрипт просто не может писать в файловую систему. Это частая проблема Laravel и его папок storage и bootstrap/cache, когда приложение крутится в Docker.

Но не тут-то было.

В чём оказалась проблема

Как оказалось, FrankenPHP по какой-то причине не может создавать подпроцессы. Я немного поиграл с setcap в Docker-образе и плюнул — сделал простой shell-скрипт, который делает то же самое, что и schedule:work:

#!/bin/sh
set -e

echo "Starting schedule loop..."

while true; do
    # Wait until the start of the next minute
    sleep $((60 - $(date +%S)))

    # Run the scheduler
    frankenphp php-cli artisan schedule:run
done

До этого момента я не испытывал никаких проблем с FrankenPHP. Он действительно сработал как drop-in replacement для образов, которые я использовал раньше.

Worker mode пока не пробовал: ожидаю, что придётся основательно дорабатывать само приложение, чтобы всё это завелось.

Логотип Данила Родина