Очереди Laravel на VPS: настройка Queue Worker и Supervisor без потери задач

Очередь Laravel и несколько PHP worker процессов под управлением Supervisor на VPS

Очереди в Laravel выносят долгие операции из HTTP-запроса: отправку писем, генерацию отчётов, обработку изображений и обращения к внешним API. Пользователь быстрее получает ответ, но у приложения появляется новый постоянно работающий компонент — queue:work. Если запустить его вручную в SSH-сессии, процесс завершится после выхода, перезагрузки VPS или необработанной ошибки. В production воркеру нужен менеджер процессов, а заданиям — понятная политика повторов, таймаутов и диагностики.

Ниже настроим очередь Laravel на Ubuntu/Debian-сервере, создадим Job-класс с зависимостью через контейнер, проверим database и Redis-драйверы, запустим воркеры под Supervisor и добавим безопасный шаг в деплой. Примеры ориентированы на современные версии Laravel 12/13, но перед применением стоит сверить команды с документацией вашей версии Laravel.

Что именно решает очередь

Без очереди контроллер может ждать внешний SMTP-сервер, API платёжного провайдера или генератор PDF. Это увеличивает время ответа и связывает пользовательский запрос с нестабильным внешним сервисом. При очереди приложение сохраняет описание задания в выбранном backend, сразу возвращает ответ, а отдельный worker забирает задание и выполняет его в фоне.

Очередь не делает операцию безошибочной и не гарантирует выполнение «ровно один раз». При неверно согласованных таймаутах одно задание может быть выдано повторно, поэтому обработчик должен быть по возможности идемпотентным: повторный запуск не должен повторно списывать деньги, создавать второй документ или отправлять несколько одинаковых уведомлений.

Где разместить проект

Для сайтов, ботов и pet-проектов можно использовать Beget: виртуальный хостинг от 420 ₽/мес., VPS от 330 ₽/мес. и тестовый период.

Перейти на Beget →

Минимальная production-схема состоит из четырёх частей:

  • приложение создаёт объект Job и помещает его в очередь;
  • database, Redis, SQS или другой backend хранит ожидающие задания;
  • php artisan queue:work выполняет задания;
  • Supervisor следит за worker-процессами и перезапускает их после штатного выхода или сбоя.

Database или Redis: с чего начать

Для небольшого проекта удобно начать с драйвера database: он не требует отдельного сервиса и позволяет видеть ожидающие задания в таблице. В новом Laravel миграция очереди обычно уже есть. Если её нет, создайте таблицы и примените миграции:

php artisan make:queue-table
php artisan make:queue-failed-table
php artisan migrate

В .env укажите:

QUEUE_CONNECTION=database

После изменения переменных окружения очистите кэш конфигурации или пересоберите его в рамках принятого процесса деплоя:

php artisan config:clear
# либо для production после проверки .env
php artisan config:cache

Database-драйвер подходит для умеренной нагрузки, но частый опрос таблицы создаёт дополнительную работу для основной СУБД. Redis обычно выбирают при большом числе коротких заданий, нескольких очередях и необходимости Laravel Horizon. Для Redis потребуется сервер Redis и расширение PhpRedis либо пакет predis/predis, после чего можно установить QUEUE_CONNECTION=redis. Не открывайте порт Redis в Интернет: слушайте loopback или закрытую сеть и ограничивайте доступ firewall.

Если проект ещё только переносится на сервер, сначала проверьте требования PHP, Composer, базы данных и фоновых процессов. Отдельный разбор есть в материале как выбрать хостинг для Laravel и PHP.

Создаём Job-класс в стиле Laravel

Сгенерируем задание для отправки чека по уже существующему заказу:

php artisan make:job SendOrderReceipt

В очередь лучше передавать идентификатор или небольшую модель, а не заранее сформированный PDF, большой массив или секреты. Саму работу выделим в сервис ReceiptSender, который Laravel внедрит в метод handle через контейнер зависимостей:

<?php

namespace AppJobs;

use AppModelsOrder;
use AppServicesReceiptSender;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationQueueQueueable;
use IlluminateSupportFacadesLog;
use Throwable;

final class SendOrderReceipt implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $maxExceptions = 2;
    public int $timeout = 60;
    public bool $failOnTimeout = true;

    public function __construct(
        public readonly int $orderId,
    ) {
        $this->onQueue('emails');
    }

    public function backoff(): array
    {
        return [10, 60, 300];
    }

    public function handle(ReceiptSender $sender): void
    {
        $order = Order::query()->findOrFail($this->orderId);

        if ($order->receipt_sent_at !== null) {
            return;
        }

        $sender->sendFor($order);

        $order->forceFill([
            'receipt_sent_at' => now(),
        ])->save();
    }

    public function failed(?Throwable $exception): void
    {
        Log::error('Receipt job failed', [
            'order_id' => $this->orderId,
            'exception' => $exception?->getMessage(),
        ]);
    }
}

Класс отвечает только за сценарий задания, а интеграция с почтой или API остаётся в отдельном сервисе. Это упрощает тестирование и замену транспорта. Проверка receipt_sent_at делает пример устойчивее к повторной выдаче задания, но в финансовом сценарии одной проверки может быть мало: используйте уникальные ключи, транзакции и идемпотентные ключи внешнего API.

Если задание создаётся внутри транзакции базы данных, отправляйте его после успешного commit. Иначе быстрый worker может попытаться прочитать заказ до его сохранения:

SendOrderReceipt::dispatch($order->id)
    ->afterCommit();

Проверяем очередь до установки Supervisor

Сначала запустите один worker в терминале от того же системного пользователя, под которым он будет работать постоянно:

cd /var/www/example/current
php artisan queue:work --queue=emails,default --sleep=3 --tries=3 --timeout=60 -v

Создайте тестовое задание, проверьте журнал Laravel и убедитесь, что оно исчезло из очереди. Команда queue:work загружает приложение один раз и остаётся в памяти, поэтому эффективнее queue:listen, но не видит новый код до перезапуска.

Для диагностики database-драйвера можно проверить таблицы jobs и failed_jobs. Для Redis полезны штатные команды Laravel и метрики, но не выводите содержимое заданий в общедоступные панели: сериализованные payload могут содержать пользовательские данные.

Устанавливаем и настраиваем Supervisor

На Ubuntu и Debian Supervisor устанавливается из системного репозитория:

sudo apt update
sudo apt install supervisor
sudo systemctl enable --now supervisor

Создайте файл /etc/supervisor/conf.d/laravel-worker.conf. Замените путь, имя пользователя и версию PHP на фактические значения сервера:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
directory=/var/www/example/current
command=/usr/bin/php artisan queue:work --queue=emails,default --sleep=3 --tries=3 --timeout=60 --max-time=3600
user=www-data
numprocs=2
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=90
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-worker.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5
environment=APP_ENV="production"

directory избавляет от зависимости от текущего каталога. Абсолютный путь к PHP исключает запуск другой CLI-версии. numprocs=2 создаёт два независимых worker-процесса; начинать с числа процессоров «на глаз» не стоит — сначала измерьте длительность заданий, память и ограничения внешних API.

stopasgroup и killasgroup помогают завершать дочерние процессы вместе с worker. Значение stopwaitsecs должно быть больше максимальной разумной длительности задания. При --timeout=60 и retry_after=90 worker успеет завершить зависшее выполнение до того, как backend повторно выдаст задание. Если timeout равен или превышает retry_after, одно задание может выполняться параллельно дважды.

Примените конфигурацию:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
sudo supervisorctl status "laravel-worker:*"

Запускайте worker от непривилегированного пользователя приложения. Ему нужны права чтения кода и .env, а также записи только в необходимые каталоги, например storage и bootstrap/cache. Не используйте root ради обхода ошибок прав.

Безопасный деплой без старого кода в памяти

Worker — долгоживущий PHP-процесс. После обновления приложения он продолжит выполнять старую версию классов, пока не будет перезапущен. В конце успешного деплоя выполните:

php artisan migrate --force
php artisan config:cache
php artisan queue:restart

queue:restart передаёт worker сигнал через cache: текущая задача завершается, процесс выходит, а Supervisor запускает его снова. Поэтому cache-драйвер должен работать и быть общим для всех узлов приложения. Не заменяйте штатный graceful restart на безусловный kill -9: он способен оборвать запись файла, отправку HTTP-запроса или транзакцию на середине.

При схеме с каталогами релизов путь /var/www/example/current обычно является симлинком. Убедитесь, что Supervisor запускает команду из актуального каталога и что новый release имеет те же права. После переключения симлинка всё равно нужен queue:restart.

Failed jobs, повторы и очистка

Просмотреть окончательно упавшие задания можно так:

php artisan queue:failed
php artisan queue:retry <uuid>
php artisan queue:retry all

Перед массовым повтором выясните причину. Если обработчик неидемпотентен, queue:retry all может повторить внешние действия. Устаревшие записи нужно удалять по понятной политике хранения, например:

php artisan queue:prune-failed --hours=168

В production полезно отслеживать не только состояние процессов Supervisor, но и длину очереди, возраст самого старого задания, число ошибок и среднее время обработки. Команда Laravel queue:monitor может генерировать событие при превышении заданного порога, но уведомление нужно реализовать отдельным listener. Для Redis-проектов с несколькими очередями рассмотрите Laravel Horizon: он добавляет балансировку и панель наблюдения, но не отменяет настройку таймаутов и безопасного доступа к интерфейсу.

Типичные ошибки

  • QUEUE_CONNECTION=sync. Задание выполняется внутри HTTP-запроса, поэтому worker ничего не получает.
  • Supervisor запускает другой PHP. CLI использует неподходящую версию или не видит расширения. Указывайте абсолютный путь и проверяйте /usr/bin/php -v.
  • Worker работает от root. Это расширяет последствия уязвимости и создаёт файлы с неудобными владельцами.
  • Нет перезапуска при деплое. Worker продолжает держать старый код и конфигурацию.
  • timeout больше retry_after. Backend повторно выдаёт задание, пока прежний процесс ещё работает.
  • Слишком много процессов. Несколько тяжёлых jobs исчерпывают RAM или лимит подключений к базе и внешнему API.
  • Секреты попадают в лог. Не записывайте payload целиком, токены, содержимое писем и персональные данные.
  • Нет идемпотентности. Повтор задания создаёт дубликаты или повторно выполняет денежную операцию.

Как проверить конфигурацию без риска для production

Не начинайте проверку с реальной рассылки или платёжного задания. Создайте отдельный безопасный Job, который пишет в журнал идентификатор теста, либо используйте staging с теми же версиями PHP, Laravel и Supervisor. В тесте должны быть видны четыре состояния: задание поставлено в очередь, один worker его забрал, обработчик завершился, а запись не попала в failed_jobs.

После запуска Supervisor проверьте процессы и последние сообщения:

sudo supervisorctl status "laravel-worker:*"
sudo tail -n 100 /var/log/supervisor/laravel-worker.log
tail -n 100 /var/www/example/current/storage/logs/laravel.log

Затем остановите один процесс командой Supervisor и убедитесь, что он возвращается в состояние RUNNING. Не завершайте произвольные PHP-процессы через killall php: на сервере могут работать PHP-FPM, другие приложения и административные задачи. Управляйте только именованной группой Supervisor.

Отдельно проверьте деплой: поставьте длинное, но безопасное тестовое задание, выполните queue:restart и убедитесь, что текущая работа завершается, а новый процесс поднимается автоматически. Если worker не выходит, проверьте cache-драйвер, права пользователя и то, не отключён ли опрос сигналов прерывания в коде приложения.

Права, секреты и журналы

Пользователь worker получает те же возможности, что и код Laravel: читает конфигурацию, обращается к базе, Redis и внешним сервисам. Поэтому компрометация Job-класса или зависимости равнозначна компрометации приложения. У системного пользователя не должно быть интерактивного входа без необходимости, прав sudo и записи в каталоги с исполняемым кодом во время обычной работы.

Файл .env храните вне публичной раздачи веб-сервера или закрывайте правилами Nginx. Не передавайте API-токены и пароли в свойствах Job: сериализованный payload хранится в database или Redis и может попасть в резервную копию, дамп либо диагностический вывод. Задание должно получать секрет через конфигурацию уже во время выполнения.

Два журнала — Laravel и Supervisor — решают разные задачи. Первый показывает прикладные исключения, второй объясняет запуск и завершение процессов. Настройте ограничение размера и ротацию обоих журналов, иначе активная ошибка может заполнить диск. При этом не скрывайте исключения без уведомления: пустой catch-блок превращает потерянные задания в невидимую проблему.

Регулярно проверяйте свободное место, память worker-процессов и число подключений к СУБД. Опция --max-time=3600 в примере периодически завершает worker штатно, после чего Supervisor создаёт новый процесс. Это помогает ограничить накопление памяти, но не заменяет поиск утечки и не гарантирует, что выбранного часа достаточно для вашего приложения.

Короткий чек-лист перед запуском

  1. Выбран реальный async-драйвер, миграции применены, backend закрыт от внешнего доступа.
  2. Job передаёт минимальные данные, использует сервис через DI и безопасно переносит повторный запуск.
  3. Задание внутри транзакции отправляется через afterCommit().
  4. --timeout меньше retry_after, а stopwaitsecs больше времени штатного завершения.
  5. Supervisor запускает worker от отдельного непривилегированного пользователя.
  6. В деплой включён php artisan queue:restart.
  7. Настроены ротация логов, контроль failed jobs и оповещение о росте очереди.

Если для Laravel нужен VPS с SSH, Supervisor и возможностью установить Redis, можно посмотреть варианты VPS Beget. Перед оплатой сопоставьте объём RAM, резервные копии и ограничения тарифа с реальной нагрузкой — сам по себе более дорогой сервер не исправляет неверную архитектуру очереди.

Итог

Надёжная очередь Laravel — это не одна команда queue:work, а согласованный набор: небольшой Job-класс, подходящий backend, идемпотентная обработка, Supervisor, корректные таймауты, graceful restart при деплое и наблюдение за ошибками. Начните с одного-двух процессов и database-драйвера, измерьте нагрузку, а затем переходите на Redis и Horizon, когда появится подтверждённая необходимость.

Для общего контекста о выборе стека можно прочитать статью почему PHP и Laravel остаются практичным выбором, а конфигурацию веб-входа для приложения удобно подготовить через генератор Nginx reverse proxy.

Официальная документация

  • Laravel Queues — драйверы, Jobs, workers, failed jobs, таймауты и Supervisor.
  • Laravel Horizon — управление Redis-очередями и наблюдение.
  • Supervisor configuration — параметры program, перезапуск, сигналы и журналы.
Хотите системно изучить программирование?

Подборка онлайн-курсов для разработчиков — Python, Java, веб и DevOps.

Смотреть курсы →

Бесплатные инструменты

Калькулятор подсетей · SSH-конфиг · Nginx · Docker Compose

Бесплатное руководство «Безопасный Linux/VPS»

14 практических страниц: SSH, firewall, обновления, резервные копии, WordPress, Docker и действия при признаках взлома.

Получить PDF →

Нужен аудит или безопасная настройка Linux/VPS?

Проверим SSH, firewall, обновления, резервные копии, Docker и журналы. Аудит — от 9 900 ₽.

Узнать состав и стоимость → 8 918 532 81 08

Статья может содержать партнёрские ссылки. Это не влияет на стоимость для вас, но помогает развивать блог.