Tous les articles
LaravelDockerDevOps

Docker + Laravel: Production-Ready Multi-Stage Setup with PHP-FPM and Nginx

//
·9 min de lecture

A complete production Docker setup for Laravel: multi-stage Dockerfile, PHP-FPM 8.3 with OPcache, Nginx reverse proxy, queue worker container, Redis, health checks, and a docker-compose.yml ready for VPS deployment.

Running Laravel in Docker for production requires more than a single Dockerfile. You need OPcache tuned for peak throughput, an Nginx container that only serves static assets from the host and proxies everything else to PHP-FPM, a separate queue worker container with a supervisor, and a Redis container for cache and sessions. This guide assembles all of it.

>Multi-stage Dockerfile

Dockerfile
# ── Stage 1: composer dependencies ──────────────────────────────────────
FROM composer:2.7 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install     --no-dev     --no-scripts     --no-interaction     --prefer-dist     --optimize-autoloader

# ── Stage 2: production image ─────────────────────────────────────────────
FROM php:8.3-fpm-alpine AS app

# System deps
RUN apk add --no-cache     bash shadow libpng-dev libjpeg-turbo-dev libwebp-dev zip unzip     oniguruma-dev icu-dev libxml2-dev

# PHP extensions
RUN docker-php-ext-configure gd --with-jpeg --with-webp  && docker-php-ext-install -j$(nproc)     pdo_mysql pgsql pdo_pgsql     gd bcmath intl opcache pcntl zip

# Redis extension
RUN pecl install redis && docker-php-ext-enable redis

# OPcache — production settings
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini

# Create non-root user
RUN addgroup -g 1000 laravel && adduser -u 1000 -G laravel -s /bin/sh -D laravel

WORKDIR /var/www/html

# Copy vendor from stage 1
COPY --from=vendor /app/vendor ./vendor

# Copy application
COPY --chown=laravel:laravel . .

# Bootstrap
RUN php artisan config:cache  && php artisan route:cache  && php artisan view:cache  && php artisan event:cache

USER laravel
EXPOSE 9000
CMD ["php-fpm"]

>OPcache configuration

ini
; docker/php/opcache.ini
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0   ; ← disable in production
opcache.save_comments=1
opcache.jit_buffer_size=64M
opcache.jit=tracing

>Nginx configuration

nginx
# docker/nginx/default.conf
server {
    listen 80;
    server_name _;
    root /var/www/html/public;
    index index.php;

    # Static assets served directly
    location ~* .(css|js|png|jpg|webp|avif|woff2|svg|ico)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # PHP via FastCGI
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ .php$ {
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300;
    }
}

>docker-compose.yml

yaml
# docker-compose.yml
services:
  app:
    build: { context: ., target: app }
    restart: unless-stopped
    volumes:
      - ./storage:/var/www/html/storage
    env_file: .env.production
    depends_on: [db, redis]
    healthcheck:
      test: ["CMD", "php", "-r", "echo 1;"]
      interval: 30s
      retries: 3

  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports: ["80:80", "443:443"]
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./public:/var/www/html/public:ro
    depends_on: [app]

  worker:
    build: { context: ., target: app }
    restart: unless-stopped
    command: php artisan queue:work redis --queue=default,high --tries=3 --timeout=90
    env_file: .env.production
    depends_on: [db, redis]

  scheduler:
    build: { context: ., target: app }
    restart: unless-stopped
    command: sh -c "while true; do php artisan schedule:run; sleep 60; done"
    env_file: .env.production
    depends_on: [db, redis]

  db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: laravel
      POSTGRES_USER: laravel
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes: [pgdata:/var/lib/postgresql/data]

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes: [redisdata:/data]

volumes:
  pgdata:
  redisdata:

>Deploy to VPS

bash
# Pull latest, rebuild, migrate with zero-downtime
docker compose pull
docker compose build --no-cache app
docker compose up -d --no-deps app worker scheduler

# Run migrations (after new containers are up)
docker compose exec app php artisan migrate --force

# Watch logs
docker compose logs -f app worker