Back to DevOpsForge Visual Builder
Production Architecture

Laravel Docker Compose with MySQL & Caddy (Auto-SSL)

Updated September 2026 Includes Dockerfile & Caddyfile Tested on Laravel 10 / 11

Deploying a production Laravel application in Docker requires solving three core challenges: optimizing PHP-FPM dependency installation with Composer caching, configuring storage directory permissions safely for non-root users, and managing HTTPS/SSL certificates without cumbersome Certbot cron jobs.

This battle-tested stack pairs Laravel with PHP 8.2, a hardened MySQL 8 database volume, and Caddy v2 for automatic Let's Encrypt SSL and reverse proxying.

Customize or Run this Stack Visually

Tweak ports, add Redis caching, or toggle S3 cloud backups with zero manual YAML typing.

1. Production `docker-compose.yml`

This configuration defines the Laravel backend, persistent MySQL database, and Caddy reverse proxy with automatic SSL renewal.

docker-compose.yml
version: '3.8'

services:
  caddy:
    image: caddy:2-alpine
    container_name: laravel_caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      backend:
        condition: service_healthy

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: laravel_app
    restart: always
    expose:
      - "8000"
    environment:
      - APP_ENV=production
      - APP_DEBUG=false
      - DB_CONNECTION=mysql
      - DB_HOST=database
      - DB_PORT=3306
      - DB_DATABASE=${DB_DATABASE:-laravel}
      - DB_USERNAME=${DB_USERNAME:-db_user}
      - DB_PASSWORD=${DB_PASSWORD:-db_password}
    depends_on:
      database:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/"]
      interval: 15s
      timeout: 5s
      retries: 3

  database:
    image: mysql:8.0-debian
    container_name: laravel_mysql
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-root_password}
      - MYSQL_DATABASE=${DB_DATABASE:-laravel}
      - MYSQL_USER=${DB_USERNAME:-db_user}
      - MYSQL_PASSWORD=${DB_PASSWORD:-db_password}
    volumes:
      - mysqldata:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  caddy_data:
  caddy_config:
  mysqldata:

2. Multi-Stage Laravel `Dockerfile`

Uses a separate Composer stage to pre-compile dependencies and copies them into a minimal Alpine runner with correct `www-data` ownership.

backend/Dockerfile
# --- Stage 1: Build Dependencies ---
FROM composer:2.5 AS vendor
WORKDIR /app
COPY composer*.json ./
RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist

# --- Stage 2: Production PHP Runner ---
FROM php:8.2-cli-alpine
WORKDIR /var/www/html

# Install required PHP extensions for Laravel & MySQL
RUN apk --no-cache add curl libxml2-dev libpng-dev libjpeg-turbo-dev freetype-dev shadow \
    && docker-php-ext-install pdo_mysql gd xml \
    && usermod -u 1000 www-data && groupmod -g 1000 www-data

# Copy pre-installed vendor dependencies
COPY --from=vendor /app/vendor/ ./vendor/
COPY . .

# Secure storage and bootstrap cache permissions
RUN chown -R www-data:www-data storage bootstrap/cache
USER www-data

EXPOSE 8000
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]

3. Production `Caddyfile`

Point your domain DNS record to your server IP, replace `yourdomain.com`, and Caddy automatically provisions Let's Encrypt SSL certificates.

Caddyfile
yourdomain.com {
    encode gzip zstd

    # Route all HTTP traffic to the Laravel container
    reverse_proxy backend:8000

    # Security Headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}