Очереди в 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 ₽/мес. и тестовый период.
Минимальная 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 создаёт новый процесс. Это помогает ограничить накопление памяти, но не заменяет поиск утечки и не гарантирует, что выбранного часа достаточно для вашего приложения.
Короткий чек-лист перед запуском
- Выбран реальный async-драйвер, миграции применены, backend закрыт от внешнего доступа.
- Job передаёт минимальные данные, использует сервис через DI и безопасно переносит повторный запуск.
- Задание внутри транзакции отправляется через
afterCommit(). --timeoutменьшеretry_after, аstopwaitsecsбольше времени штатного завершения.- Supervisor запускает worker от отдельного непривилегированного пользователя.
- В деплой включён
php artisan queue:restart. - Настроены ротация логов, контроль 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.
14 практических страниц: SSH, firewall, обновления, резервные копии, WordPress, Docker и действия при признаках взлома.
Проверим SSH, firewall, обновления, резервные копии, Docker и журналы. Аудит — от 9 900 ₽.
Статья может содержать партнёрские ссылки. Это не влияет на стоимость для вас, но помогает развивать блог.

