Supervisor is a process manager. For a Laravel app, it keeps queue workers, the scheduler, and any custom long-running processes alive. systemd can do the same job, but Supervisor's config language is friendlier and its log handling is built-in.
Here's the configuration I run in production.
Why Not Just php artisan queue:work?
If you ssh to your server and run php artisan queue:work, the moment your shell disconnects the worker dies. nohup and & postpone the problem; they don't solve it. When the worker crashes (and it will — OOM, network blip, deploy), nothing restarts it.
Supervisor solves both: it daemonizes processes and auto-restarts them on failure.
Directory Layout
/etc/supervisor/conf.d/
laravel-worker.conf # queue workers
laravel-scheduler.conf # scheduler
laravel-horizon.conf # if using Horizon instead of queue:work
laravel-reverb.conf # if using Reverb websockets
One file per concern. Easier to reason about than one mega-file.
Queue Worker
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/portfolio/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --max-jobs=1000
autostart=true
autorestart=true
user=deploy
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/portfolio/shared/storage/logs/worker.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stopwaitsecs=3600
Key choices:
numprocs=2— two worker processes. Bump up if you have CPU headroom and your queue depth grows.--max-time=3600— workers exit cleanly after an hour. Supervisor restarts them. This prevents memory leaks from accumulating indefinitely.--max-jobs=1000— same idea, but counted by jobs processed.stopwaitsecs=3600— when Supervisor sends SIGTERM, give the worker a full hour to finish its current job before sending SIGKILL. Critical for long jobs.%(process_num)02d— Supervisor expands this to00,01per process. Required whennumprocs > 1.
The Scheduler
[program:laravel-scheduler]
command=php /var/www/portfolio/current/artisan schedule:work
autostart=true
autorestart=true
user=deploy
redirect_stderr=true
stdout_logfile=/var/www/portfolio/shared/storage/logs/scheduler.log
stdout_logfile_maxbytes=50MB
schedule:work is the modern replacement for the cron entry that runs schedule:run every minute. Supervisor keeps it alive; the command's internal loop ticks every 60 seconds and dispatches scheduled tasks.
If you prefer cron (some teams do for visibility):
* * * * * deploy cd /var/www/portfolio/current && php artisan schedule:run >> /dev/null 2>&1
I prefer Supervisor — one process manager, all logs in one place.
Horizon
If you're using Horizon, replace laravel-worker with:
[program:laravel-horizon]
command=php /var/www/portfolio/current/artisan horizon
autostart=true
autorestart=true
user=deploy
stopwaitsecs=3600
redirect_stderr=true
stdout_logfile=/var/www/portfolio/shared/storage/logs/horizon.log
Horizon spawns its own worker children based on config/horizon.php. Supervisor manages the parent. numprocs=1 (the default) is correct here — Horizon does the multiplexing.
Deploy-Time Restart
Workers cache code. After a deploy, they're running the old code until restarted:
php artisan queue:restart
sudo supervisorctl restart laravel-worker:*
queue:restart writes a sentinel to the cache. Workers check the sentinel between jobs and exit gracefully. Supervisor restarts them with the new code.
Critical: restart (not stop then start). Restart is one operation; stop-then-start has a window where workers are completely down.
Permissions
Supervisor runs as root by default. The user=deploy directive drops privileges before exec'ing. The deploy user needs:
- Read access to the codebase (
/var/www/portfolio/) - Write access to logs (
storage/logs/) - Permission to write to Redis and the database
sudo useradd -r -s /bin/bash deploy
sudo chown -R deploy:deploy /var/www/portfolio
Status & Logs
sudo supervisorctl status
# laravel-worker:laravel-worker_00 RUNNING pid 12345, uptime 2:14:08
# laravel-worker:laravel-worker_01 RUNNING pid 12346, uptime 2:14:08
# laravel-scheduler RUNNING pid 12347, uptime 2:14:08
sudo supervisorctl tail -f laravel-worker:laravel-worker_00
The tail -f is invaluable when debugging a job that misbehaves only in production.
Healthchecks
I run a side script that asserts Supervisor processes are RUNNING and pages me if anything is FATAL:
#!/usr/bin/env bash
EXPECTED=("laravel-worker:laravel-worker_00" "laravel-worker:laravel-worker_01" "laravel-scheduler")
for proc in "${EXPECTED[@]}"; do
status=$(sudo supervisorctl status "$proc" | awk '{print $2}')
if [ "$status" != "RUNNING" ]; then
curl -X POST "$ALERT_WEBHOOK" -d "Supervisor process $proc is $status"
fi
done
Cron'd every 5 minutes. Stupid simple, works.
Al Amin Ahamed
Senior software engineer & AI practitioner. Laravel, PHP, WordPress plugins, WooCommerce extensions.
About me →More from the blog
← Older
Cloudflare in Front of Laravel: Real IPs, Trusted Proxies, Locked-Down Origins
Newer →
Caddy + Laravel: A 50-Line Production Config
One email a month. No noise.
What I shipped, what I read, occasional deep dive. Unsubscribe anytime.
Check your inbox — confirmation link sent.