You deploy your Laravel app to Docker, push a queue job, and 30 minutes later — silence. The worker is gone. No logs. No errors. Just a dead container or a zombie process that is not picking up jobs.
This is one of the most common production problems Laravel developers face, and it is almost always preventable. This guide covers every reason workers die in Docker and the exact configuration to fix each one.
1. The 4 Reasons Laravel Queue Workers Die in Docker
Understanding which cause is killing your workers is important because the fix is different for each:
- Memory Leaks — PHP accumulates memory every job. Without a restart limit, memory grows until Docker OOM killer terminates the process.
- Uncaught Exceptions — Exceptions outside
handle()(in middleware, serialization, or polling) crash the entire worker process, not just the job. - OOM Kill (SIGKILL) — Docker containers have memory limits. When the limit is hit, Linux sends SIGKILL — which cannot be caught or deferred. The worker dies mid-job.
- No Process Manager — Without Supervisor or Horizon, a dead worker simply stays dead. No one restarts it. Jobs pile up silently.
2. --max-time and --max-jobs: The Core Fix for Memory Leaks
PHP is a long-running process when used as a queue worker. Each job it processes can leave behind small amounts of memory that are never garbage collected — Eloquent model caches, event listeners, static singletons. Over hours, this adds up.
The solution is not to fix every memory leak. The solution is to restart the worker periodically before it accumulates too much memory. Laravel provides dedicated flags for this:
| Flag | What it does | Recommended value |
|---|---|---|
--max-time=N | Exit gracefully after N seconds (finishes current job first) | 3600 (1 hour) |
--max-jobs=N | Exit after processing N jobs | 500 |
--memory=N | Exit if memory exceeds N MB (checked between jobs) | 256 |
--sleep=N | Seconds to wait when queue is empty | 3 |
--tries=N | Max attempts before marking a job as failed | 3 |
--timeout=N | Seconds before a single job is killed | 60 |
Here is the command you should run in production:
# Exits gracefully before leaking memory, Supervisor restarts automatically
php artisan queue:work redis --queue=critical,default --max-jobs=500 --max-time=3600 --memory=256 --sleep=3 --tries=3 --timeout=60
3. Supervisor Configuration for Docker
Supervisor is the standard process manager for Laravel queue workers on Linux. It keeps workers running continuously — when a worker exits (from --max-time, --max-jobs, or a crash), Supervisor immediately starts a new one.
In docker-compose.yml, add a dedicated worker service:
services:
app:
build: .
# Main web service — serves HTTP requests
queue-worker:
build: .
command: supervisord -c /etc/supervisor/conf.d/laravel-worker.conf -n
restart: unless-stopped
depends_on:
- app
environment:
- QUEUE_CONNECTION=redis
volumes:
- ./storage:/var/www/html/storage
Create the Supervisor config at /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-jobs=500 --max-time=3600 --memory=256 --timeout=60 --queue=critical,default
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600
4. Production Dockerfile Setup
Here is a production-ready Dockerfile that installs Supervisor and bundles the worker config:
FROM php:8.3-fpm-alpine
# Install dependencies
RUN apk add --no-cache supervisor redis
# Install PHP extensions
RUN docker-php-ext-install pdo pdo_mysql opcache
RUN pecl install redis && docker-php-ext-enable redis
WORKDIR /var/www/html
# Copy app files
COPY . .
RUN composer install --no-dev --optimize-autoloader
# Copy Supervisor config
COPY docker/supervisor/laravel-worker.conf /etc/supervisor/conf.d/
# Set permissions
RUN chown -R www-data:www-data storage bootstrap/cache
RUN chmod -R 775 storage bootstrap/cache
# Start Supervisor in foreground
CMD ["supervisord", "-n", "-c", "/etc/supervisor/conf.d/laravel-worker.conf"]
5. Handling OOM Kills and Memory Limits
An OOM kill (Out of Memory kill) is different from a graceful exit. When Docker memory limit is hit, Linux sends SIGKILL — which cannot be caught or handled by PHP. The worker dies immediately, mid-job.
services:
queue-worker:
mem_limit: 512m
mem_reservation: 256m
The rule: --memory × numprocs should always be less than the container mem_limit. Give 20–30% headroom for the base PHP process and Supervisor overhead.
6. Uncaught Exceptions and Worker Crashes
There are two types of exceptions in a Laravel queue worker:
- Exception inside
handle()— caught by Laravel, job is marked failed, worker continues. - Exception outside
handle()— not caught, worker process crashes. This includes errors in constructor, serialization, queue middleware, or Redis connection drops.
// Safe — exception inside handle() is caught by Laravel
class ProcessOrder implements ShouldQueue
{
public function handle(): void
{
$this->processPayment();
}
}
// Dangerous — exception in constructor crashes the entire worker process
class BadJob implements ShouldQueue
{
public function __construct(
public readonly Order $order
) {}
}
7. Horizon vs queue:work in Docker
| Feature | queue:work + Supervisor | Laravel Horizon |
|---|---|---|
| Redis required | No | Yes (mandatory) |
| Dashboard UI | No | Yes (real-time metrics) |
| Auto-scaling workers | No | Yes |
| Metrics & throughput | No | Yes |
| Failed job UI | No (artisan only) | Yes |
| Docker complexity | Simple | Moderate |
| Worker restart handling | Supervisor | Automatic (built-in) |
# Graceful shutdown on SIGTERM in container entrypoint
trap "php artisan horizon:terminate" SIGTERM SIGINT
php artisan horizon
wait
8. Production Deployment Checklist
Before releasing queue workers to production Docker, verify each of these items:
# 1. queue:work command has --max-time, --max-jobs, --memory
php artisan queue:work redis --max-jobs=500 --max-time=3600 --memory=256
# 2. Failed jobs table exists and migrated
php artisan queue:failed-table
php artisan migrate
# 3. Always restart workers after deployment in your CI/CD script
php artisan queue:restart
Laravel queue workers die in Docker for predictable, fixable reasons. The core solution is always the same: use --max-time and --max-jobs to restart workers before they leak memory, and use Supervisor (or Horizon) with autorestart=true to keep them running continuously.