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.
This configuration defines the Laravel backend, persistent MySQL database, and Caddy reverse proxy with automatic SSL renewal.
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:
Uses a separate Composer stage to pre-compile dependencies and copies them into a minimal Alpine runner with correct `www-data` ownership.
# --- 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"]
Point your domain DNS record to your server IP, replace `yourdomain.com`, and Caddy automatically provisions Let's Encrypt SSL certificates.
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"
}
}