DEV Community

Cover image for Lesson 10: Run PHP and Nginx with Supervisor in Docker
Muhammad Yasir Amin
Muhammad Yasir Amin

Posted on Originally published at yasiramin.dev

Lesson 10: Run PHP and Nginx with Supervisor in Docker

Laravel Docker Lessons · Lesson 10

Run Laravel PHP-FPM and Nginx in one optional Docker container with Supervisor, process status checks, foreground logging and graceful shutdown.

This written tutorial follows the actual lesson-10 source snapshot. The published Urdu video lesson is available alongside this written guide.

Continue from Lesson 9, or browse the complete blog.

Earlier lessons use a separate PHP-FPM container and Nginx container. This lesson adds an optional combined container to explain process management: Supervisor starts and monitors both PHP-FPM and Nginx in the foreground. The normal multi-container Compose setup remains available.

Complete file checklist before running commands

Keep docker-compose.yaml as the base service definition and create docker-compose.supervisor.yaml separately. This Supervisor file is standalone on the command line, but its extends entries read web and db from the base file. Do not overwrite the base: a self-reference or missing inherited service will prevent Compose from loading. Create all named configuration files before building. The Dockerfile.supervisor base image must be built from this lesson checkout first.

File Action from the previous lesson Exact source
.docker/nginx/supervisor.conf Create this file; full code included View complete file
.docker/supervisor/supervisord.conf Create this file; full code included View complete file
Dockerfile.supervisor Create this file; full code included View complete file
docker-compose.supervisor.yaml Create this file; full code included View complete file
Dockerfile Keep the inherited file; inspect the exact snapshot View complete file
docker-compose.yaml Keep the inherited file; inspect the exact snapshot View complete file

Use the committed lock files from the same tag. They are download/source links rather than abbreviated code samples: composer.lock, package-lock.json.

Create the configuration directories before saving the files:

New-Item -ItemType Directory -Force .docker/supervisor, .docker/nginx
Enter fullscreen mode Exit fullscreen mode

docker-compose.yaml

x-laravel-environment: &laravel-environment
  DB_URL: ""
  DB_SOCKET: ""
  DB_CONNECTION: mysql
  DB_HOST: db
  DB_PORT: 3306
  DB_DATABASE: laravel_app
  DB_USERNAME: root
  DB_PASSWORD: root
  QUEUE_CONNECTION: database
  DB_QUEUE_RETRY_AFTER: 90

x-laravel-service: &laravel-service
  build:
    context: .
    dockerfile: Dockerfile
  image: my-laravel-app
  working_dir: /var/www/html
  user: "www-data"
  volumes:
    - .:/var/www/html
    - /var/www/html/vendor
    - /var/www/html/node_modules
    - /var/www/html/storage
    - /var/www/html/bootstrap/cache
  environment:
    <<: *laravel-environment
    SCHEDULER_DEMO_ENABLED: "false"
  depends_on:
    db:
      condition: service_healthy

services:
  web:
    <<: *laravel-service

  scheduler:
    <<: *laravel-service
    profiles: ["scheduler"]
    init: true
    command: ["php", "artisan", "schedule:work", "--no-interaction"]
    restart: unless-stopped
    environment:
      <<: *laravel-environment
      CACHE_STORE: file
      SCHEDULER_DEMO_ENABLED: "true"

  worker:
    <<: *laravel-service
    profiles: ["queue"]
    init: true
    command: ["php", "artisan", "queue:work", "database", "--queue=emails", "--sleep=1", "--tries=3", "--timeout=30", "--max-time=3600", "--no-interaction"]
    restart: unless-stopped
    stop_grace_period: 45s

  nginx:
    image: nginx:latest
    ports:
      - "${APP_PORT:-8080}:80"
    volumes:
      - .:/var/www/html:ro
      - ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - web

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel_app
    ports:
      - "${DB_FORWARD_PORT:-3308}:3306"
    volumes:
      - dbdata:/var/lib/mysql
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p\"$${MYSQL_ROOT_PASSWORD}\" --silent"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 20s

volumes:
  dbdata:
Enter fullscreen mode Exit fullscreen mode

docker-compose.supervisor.yaml

services:
  app:
    extends:
      file: docker-compose.yaml
      service: web
    build:
      context: .
      dockerfile: Dockerfile.supervisor
    image: laravel-supervisor-demo
    user: root
    ports:
      - "${APP_PORT:-8080}:80"
    restart: unless-stopped
    stop_grace_period: 45s
  db:
    extends:
      file: docker-compose.yaml
      service: db
volumes:
  dbdata:
Enter fullscreen mode Exit fullscreen mode

Check that both files exist with their exact names before running Compose:

Get-Item .\docker-compose.yaml, .\docker-compose.supervisor.yaml
Enter fullscreen mode Exit fullscreen mode

Recording preparation and follow-up checks

Build the base PHP image before the combined Supervisor image. This topology names the Laravel service app, so use exec app. FastCGI targets 127.0.0.1:9000 inside the combined container; the normal separate Nginx service uses web:9000. Stop the previous selected stack before reusing its browser port.

Before recording, follow the recording checklist and PowerShell preflight. Current main excludes generated bootstrap config cache and public/hot from Docker build context. For historical tags, clear old host config cache before rebuilding and renew PHP runtime volumes so earlier database settings are not reused.

Build the combined image

Dockerfile.supervisor builds from the project's my-laravel-app image. Build the base image first so the combined image includes this checkout's PHP extensions, Composer, Node, npm and application dependencies. It installs Debian's Nginx and Supervisor packages, copies the two configuration files and uses Supervisor as its foreground entry command.

docker compose build web
docker compose -f docker-compose.supervisor.yaml build app
Enter fullscreen mode Exit fullscreen mode

The combined container uses root for Supervisor and the Nginx master, allowing Nginx to bind port 80. Nginx workers and PHP-FPM pool workers use www-data. Supervisor's control interface is a local Unix socket; no public management TCP port is added. Do not expose FastCGI port 9000 to the host.

Start the optional topology

Prepare .env and its application key using Lesson 8 first. Stop the previous stack to free port 8080, retaining its volumes. The standalone Supervisor Compose file reuses the existing MySQL service and dbdata named volume when you use the same Compose project name.

docker compose down
docker compose -f docker-compose.supervisor.yaml up -d db app
docker compose -f docker-compose.supervisor.yaml exec --user www-data app php artisan config:clear
docker compose -f docker-compose.supervisor.yaml exec --user www-data app php artisan migrate --force
docker compose -f docker-compose.supervisor.yaml exec app supervisorctl status
docker compose -f docker-compose.supervisor.yaml exec app nginx -t
Enter fullscreen mode Exit fullscreen mode

Expect nginx and php-fpm to be RUNNING. Open http://localhost:8080/queue-demo/mail-preview for a Blade page that does not send email. /scheduler-demo also remains available after SQL migrations. APP_PORT can change the host application port.

Understand the two configurations

.docker/supervisor/supervisord.conf enables foreground mode, a local control socket, automatic restart of unexpectedly exited services and logs to Docker stdout/stderr. Rotation is disabled for these special output streams. PHP-FPM runs with -F; Nginx runs with daemon off;. Neither forks into the background and disappears from supervision.

.docker/nginx/supervisor.conf points FastCGI to 127.0.0.1:9000, because both processes share one container. The earlier separate Nginx container uses web:9000; copying that hostname into this combined topology would be incorrect. Both configurations serve Laravel's public directory and pass unmatched requests to index.php.

Supervisor starts PHP-FPM before Nginx. Process priority controls start order, not application readiness; a very early request can still need a retry. The database health check separately gates container startup. Laravel migrations remain an explicit command rather than running every time a process restarts.

Inspect process recovery and logs

docker compose -f docker-compose.supervisor.yaml exec app supervisorctl restart php-fpm
docker compose -f docker-compose.supervisor.yaml exec app supervisorctl status
docker compose -f docker-compose.supervisor.yaml logs --tail=40 app
Enter fullscreen mode Exit fullscreen mode

Restarting PHP briefly interrupts requests; use it only in this local teaching demo. Supervisor restarts processes inside the container, while Docker's restart policy handles a stopped container. These are different lifecycles. This example does not also start a queue worker or scheduler inside Supervisor, preventing duplicate background processing.

Stop gracefully

docker compose -f docker-compose.supervisor.yaml down
Enter fullscreen mode Exit fullscreen mode

The image uses SIGTERM for Supervisor. It forwards configured shutdown signals to the child process groups; PHP-FPM and Nginx receive QUIT for a graceful exit. The Compose grace period allows time for shutdown. Omit -v to retain the database. For independent scaling, the existing separate-container topology is still available.

How the implementation fits together

Lesson 10: Run PHP and Nginx with Supervisor in Docker implementation diagram

Diagram of the implementation in the lesson-10 tag; this is not a production monitoring screenshot.

Configuration and source code

The following files are read directly from this lesson’s Git tag. Later lessons add further capabilities; they do not change this snapshot.

Dockerfile

FROM php:8.2-fpm

WORKDIR /var/www/html

RUN apt-get update && apt-get install -y \
    libzip-dev \
    libpq-dev \
    unzip \
    && docker-php-ext-install zip pdo_mysql pdo_pgsql pcntl \
    && rm -rf /var/lib/apt/lists/*

RUN pecl install mongodb-1.21.2 \
    && docker-php-ext-enable mongodb

COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
ENV COMPOSER_HOME=/tmp/composer

COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node
COPY --from=node:22-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
    && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
    && node --version && npm --version
ENV NPM_CONFIG_CACHE=/tmp/npm-cache

COPY --chown=www-data:www-data . /var/www/html

RUN COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --prefer-dist --optimize-autoloader \
    && mkdir -p node_modules \
    && chown -R www-data:www-data vendor storage bootstrap/cache node_modules /tmp/composer \
    && chmod -R 775 storage bootstrap/cache
Enter fullscreen mode Exit fullscreen mode

Dockerfile.supervisor

FROM my-laravel-app:latest

USER root

RUN apt-get update && apt-get install -y --no-install-recommends nginx supervisor \
    && rm -rf /var/lib/apt/lists/* \
    && rm -f /etc/nginx/sites-enabled/default

COPY .docker/nginx/supervisor.conf /etc/nginx/conf.d/laravel.conf
COPY .docker/supervisor/supervisord.conf /etc/supervisor/supervisord.conf

EXPOSE 80
STOPSIGNAL SIGTERM
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
Enter fullscreen mode Exit fullscreen mode

.docker/supervisor/supervisord.conf

[unix_http_server]
file=/tmp/supervisor.sock
chmod=0700

[supervisord]
nodaemon=true
user=root
pidfile=/tmp/supervisord.pid
logfile=/dev/stdout
logfile_maxbytes=0

[rpcinterface:supervisor]
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock

[program:php-fpm]
command=/usr/local/sbin/php-fpm -F
priority=10
autostart=true
autorestart=true
startsecs=3
stopsignal=QUIT
stopwaitsecs=20
stopasgroup=true
killasgroup=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
priority=20
autostart=true
autorestart=true
startsecs=3
stopsignal=QUIT
stopwaitsecs=20
stopasgroup=true
killasgroup=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Enter fullscreen mode Exit fullscreen mode

.docker/nginx/supervisor.conf

server {
    listen 80;
    index index.php index.html;
    root /var/www/html/public;
    access_log /dev/stdout;
    error_log /dev/stderr;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
    }

    location ~ /\.ht {
        deny all;
    }
}
Enter fullscreen mode Exit fullscreen mode

Verification performed

The optional combined image built successfully. Supervisor reported both nginx and php-fpm RUNNING, nginx -t passed, and the Blade email preview returned HTTP 200. After supervisorctl restart php-fpm, its process ID changed and the page still returned HTTP 200. Docker stop delivered SIGTERM to Supervisor; both child services exited with status 0.

References


Originally published at yasiramin.dev.

Top comments (0)