Muhammad Yasir Amin

Loading website…

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Lesson 10: Run PHP and Nginx with Supervisor in Docker

✍️ Build & Learn8 min read

Lesson 10: Run PHP and Nginx with Supervisor in Docker

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineer•October 18, 2026
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 written guide is ready before the video recording.

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.

FileAction from the previous lessonExact source
.docker/nginx/supervisor.confCreate this file; full code includedView complete file
.docker/supervisor/supervisord.confCreate this file; full code includedView complete file
Dockerfile.supervisorCreate this file; full code includedView complete file
docker-compose.supervisor.yamlCreate this file; full code includedView complete file
DockerfileKeep the inherited file; inspect the exact snapshotView complete file
docker-compose.yamlKeep the inherited file; inspect the exact snapshotView 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:

Code
New-Item -ItemType Directory -Force .docker/supervisor, .docker/nginx

docker-compose.yaml

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:

docker-compose.supervisor.yaml

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:

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

Code
Get-Item .\docker-compose.yaml, .\docker-compose.supervisor.yaml

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.

Shell
docker compose build web
docker compose -f docker-compose.supervisor.yaml build app

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.

Shell
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

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

Shell
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

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

Shell
docker compose -f docker-compose.supervisor.yaml down

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

Code
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

Dockerfile.supervisor

Code
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"]

.docker/supervisor/supervisord.conf

Code
[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

.docker/nginx/supervisor.conf

Code
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;
    }
}

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

Article Index & Topic Tags
DockerLaravelPhpNginxLaravel Docker LessonsPhp-FpmSupervisor

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.