Running a Queue Worker in Docker Compose

Dispatching a job to Symfony Messenger or a Laravel queue doesn't run it — something still has to consume the queue. That "something" needs to be a long-running process, which doesn't fit naturally into the container that's also answering web requests.

Why not just run the worker inside the PHP-FPM container

PHP-FPM manages a pool of worker processes dedicated to handling incoming FastCGI requests from Nginx. Starting a long-running messenger:consume or queue:work command inside that same container means either running it as a detached background process PHP-FPM doesn't manage or supervise (so nothing restarts it if it dies), or dedicating one of the FPM pool workers to it indefinitely (so it competes with actual HTTP traffic for the same limited pool). Neither is a real solution — the queue consumer needs its own container.

A dedicated worker service

The fix is a second service using the same image as your PHP application, but overriding the command it runs:

services:
  php:
    image: php:8.5-fpm
    volumes:
      - .:/var/www/html

  worker:
    image: php:8.5-fpm
    volumes:
      - .:/var/www/html
    working_dir: /var/www/html
    command: php bin/console messenger:consume async -vv
    restart: unless-stopped
    depends_on:
      - php

Laravel's equivalent command:

    command: php artisan queue:work --tries=3

Why restart: unless-stopped matters here specifically

Queue consumer commands are expected to eventually exit — a fatal error in a job handler, a memory limit hit after processing thousands of messages, an OOM kill. Without a restart policy, the worker container simply stops, and nothing about your application looks broken from the outside: the web app still responds, jobs still get queued, they just silently stop being processed. restart: unless-stopped means Docker brings the worker back automatically, the same way it would for a database or cache service, rather than treating "the queue consumer" as a one-shot command.

Scaling to multiple consumers

Once a single worker isn't keeping up, run several in parallel:

docker compose up -d --scale worker=3

This only works cleanly if the worker service has no fixed container_name and doesn't publish a host port (both would conflict across replicas) — the version generated by the tool on this site is scale-safe by default for exactly this reason.

Common pitfall: the worker starting before its transport is ready

If jobs are queued via Redis or RabbitMQ, the worker container can start and attempt to connect before that service has finished initializing, especially on a fresh docker compose up. depends_on alone only waits for the container to start, not for the queue to be ready to accept connections — see Docker Compose healthchecks explained for how to make the worker actually wait.

Try it yourself

Generate a ready-to-run Docker Compose setup for your stack — compose.yml, nginx config, .env, and a README, in seconds.

Open the generator →