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:

  1. Memory Leaks — PHP accumulates memory every job. Without a restart limit, memory grows until Docker OOM killer terminates the process.
  2. Uncaught Exceptions — Exceptions outside handle() (in middleware, serialization, or polling) crash the entire worker process, not just the job.
  3. 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.
  4. 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:

FlagWhat it doesRecommended value
--max-time=NExit gracefully after N seconds (finishes current job first)3600 (1 hour)
--max-jobs=NExit after processing N jobs500
--memory=NExit if memory exceeds N MB (checked between jobs)256
--sleep=NSeconds to wait when queue is empty3
--tries=NMax attempts before marking a job as failed3
--timeout=NSeconds before a single job is killed60

Here is the command you should run in production:

bash
# 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:

yaml
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:

ini
[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:

dockerfile
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.

yaml
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.
php
// 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

Featurequeue:work + SupervisorLaravel Horizon
Redis requiredNoYes (mandatory)
Dashboard UINoYes (real-time metrics)
Auto-scaling workersNoYes
Metrics & throughputNoYes
Failed job UINo (artisan only)Yes
Docker complexitySimpleModerate
Worker restart handlingSupervisorAutomatic (built-in)
bash
# 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:

bash
# 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.