Deploying Laravel with Docker: Setup Horizon, Reverb, Scheduler & Inertia SSR
Deploy production-ready Laravel applications using Docker with this comprehensive guide. Learn to configure Horizon queue workers, Reverb WebSockets, Inertia.js SSR, and choose between FPM-NGINX or FrankenPHP runtimes with Traefik integration.
Introduction
This tutorial demonstrates how to deploy a Laravel application using Docker with a production-optimized setup. We'll configure all essential Laravel services including background jobs with Horizon, WebSocket support via Reverb, the task scheduler, and Inertia.js SSR rendering.
Our stack uses Traefik as a reverse proxy and leverages the serversideup/php Docker images, which extend official PHP images with production-ready security hardening, performance optimizations, and developer-friendly defaults.
Prerequisites
Before starting, ensure you have:
- A Laravel application with the following features already configured:
- Laravel Horizon (if using queues)
- Laravel Reverb (if using WebSockets)
- Inertia.js with SSR (if using React/Vue with server-side rendering)
- Laravel Scheduler (if using scheduled tasks)
- Traefik reverse proxy configured with Docker (follow the official Traefik Docker setup guide)
- Docker and Docker Compose installed on your deployment server
- Basic familiarity with Docker concepts and Laravel deployment requirements
Choosing Your PHP Runtime Image
The serversideup/php project provides two primary image variants optimized for different use cases:
FPM-NGINX combines PHP-FPM with NGINX as a reverse proxy in a single container. This traditional architecture offers the best balance of performance, stability, and compatibility for most Laravel applications. It's the recommended choice for standard deployments.
FrankenPHP is a modern application server built on the Caddy web server that runs PHP and the web server in a single process. This variant is designed for Laravel Octane and eliminates the complexity of managing separate PHP-FPM and web server processes. Choose this if you're using Octane for enhanced performance.
We'll cover deployment using both variants, starting with FPM-NGINX.
Deployment with FPM-NGINX
Configuring Trusted Proxies
Since your application runs behind Traefik, you need to configure Laravel to trust the proxy for proper IP address detection and URL generation.
For Laravel 11+, add the following to bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})
For Laravel 10 and earlier, update app/Http/Middleware/TrustProxies.php:
protected $proxies = '*';
Creating the Dockerfile
Create a Dockerfile in the root of your Laravel project:
FROM serversideup/php:8.4-fpm-nginx AS laravel-app
USER root
# Install Node.js and npm for frontend asset compilation
RUN apt-get update && apt-get install -y nodejs npm
WORKDIR /var/www/html
# Copy dependency files first (enables Docker layer caching)
COPY composer.json composer.lock ./
COPY package.json package-lock.json ./
# Change ownership to www-data for proper permissions
RUN chown -R www-data:www-data /var/www/html
USER www-data
# Install PHP dependencies
RUN composer install --no-dev --no-scripts --optimize-autoloader
# Install JavaScript dependencies
RUN npm install
# Copy the full project with correct ownership
COPY --chown=www-data:www-data . .
# Build frontend assets with SSR support
RUN npm run build:ssr
This Dockerfile follows best practices by copying dependency files before the full project, which allows Docker to cache layers and speed up subsequent builds when only application code changes.
Configuring Docker Compose
Create a docker-compose.yml file in your project root:
services:
app:
build:
context: .
dockerfile: Dockerfile
target: laravel-app
image: laravel-app
restart: unless-stopped
env_file:
- .env
environment:
PHP_OPCACHE_ENABLE: "1"
AUTORUN_ENABLED: "true" # Automatically runs Laravel migrations and optimizations on container start
HEALTHCHECK_PATH: "/up" # Laravel 11+ health check endpoint
volumes:
- storage:/var/www/html/storage
- bootstrap-cache:/var/www/html/bootstrap/cache
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.testing-laravel.rule=Host(`${APP_HOST}`)"
- "traefik.http.routers.testing-laravel.entrypoints=websecure"
- "traefik.http.routers.testing-laravel.tls=true"
- "traefik.http.services.testing-laravel.loadbalancer.server.port=8080"
networks:
- laravel
- traefik-proxy
mysql:
image: mysql:8.0
restart: always
env_file:
- .env
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- mysql_data_storage:/var/lib/mysql
networks:
- laravel
redis:
image: redis:7
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- laravel
ssr:
image: laravel-app
pull_policy: never
command: php artisan inertia:start-ssr
restart: always
depends_on:
app:
condition: service_started
volumes:
- storage:/var/www/html/storage
- bootstrap-cache:/var/www/html/bootstrap/cache
env_file:
- .env
networks:
- laravel
scheduler:
image: laravel-app
pull_policy: never
restart: unless-stopped
depends_on:
app:
condition: service_started
command: php artisan schedule:work
volumes:
- storage:/var/www/html/storage
- bootstrap-cache:/var/www/html/bootstrap/cache
env_file:
- .env
networks:
- laravel
horizon:
image: laravel-app
pull_policy: never
restart: unless-stopped
depends_on:
app:
condition: service_started
redis:
condition: service_healthy
command: ["php", "/var/www/html/artisan", "horizon"]
stop_signal: SIGTERM
healthcheck:
test: ["CMD", "healthcheck-horizon"]
start_period: 10s
env_file:
- .env
volumes:
- storage:/var/www/html/storage
- bootstrap-cache:/var/www/html/bootstrap/cache
networks:
- laravel
reverb:
image: laravel-app
pull_policy: never
command: php artisan reverb:start
restart: always
depends_on:
app:
condition: service_started
env_file:
- .env
volumes:
- storage:/var/www/html/storage
- bootstrap-cache:/var/www/html/bootstrap/cache
labels:
- "traefik.enable=true"
- "traefik.http.routers.laravel-reverb.rule=Host(`${REVERB_HOST}`)"
- "traefik.http.routers.laravel-reverb.entrypoints=websecure"
- "traefik.http.routers.laravel-reverb.tls=true"
- "traefik.http.services.laravel-reverb.loadbalancer.server.port=${REVERB_PORT}"
networks:
- laravel
- traefik-proxy
volumes:
mysql_data_storage:
storage:
bootstrap-cache:
networks:
laravel:
driver: bridge
traefik-proxy:
external: true
Understanding Service Configuration
Application Service: The main Laravel application container handles web requests through NGINX and PHP-FPM. The AUTORUN_ENABLED environment variable triggers automatic execution of common deployment tasks like running migrations and clearing caches on container startup. The HEALTHCHECK_PATH points to Laravel 11's built-in /up health check route (for earlier Laravel versions, you'll need to create this route manually or adjust the path).
Database and Cache Services: MySQL and Redis both include health checks to ensure dependent services only start after these are fully operational.
Inertia SSR Service: Runs a separate Node.js process for server-side rendering of your Inertia.js components. Skip this service if you're not using Inertia with SSR.
Scheduler Service: Executes schedule:work to run your scheduled tasks without requiring cron configuration.
Horizon Service: Manages your queue workers with monitoring and configuration through the Horizon dashboard. The healthcheck-horizon command is provided by the serversideup image.
Reverb Service: Handles WebSocket connections for real-time features. The Traefik configuration routes WebSocket requests to this service using the configured REVERB_HOST and REVERB_PORT.
Environment Configuration
Make sure to have the following to your .env file:
APP_HOST=your-app.example.com
REVERB_HOST=ws.your-app.example.com
REVERB_PORT=8080
DB_DATABASE=db_name
DB_USERNAME=db_username
DB_PASSWORD=secure-password
For local development with Traefik, use the default Docker hostname format:
APP_HOST=laravel-app.docker.localhost
REVERB_HOST=reverb.docker.localhost
REVERB_PORT=8080
DB_DATABASE=db_name
DB_USERNAME=db_username
DB_PASSWORD=secure-password
Ensure your traefik-proxy network name matches the network you configured when setting up Traefik. If you used a different name, update the networks section accordingly.
Deployment with FrankenPHP (Alternative)
If you're using Laravel Octane for enhanced performance, you can deploy with FrankenPHP instead of FPM-NGINX.
Dockerfile Modifications
Change the base image in your Dockerfile:
FROM serversideup/php:8.4-frankenphp AS laravel-app
The rest of the Dockerfile remains the same. Note that you should install Laravel Octane in your project before building:
composer require laravel/octane
php artisan octane:install --server=frankenphp
Docker Compose Adjustments
Replace the app service configuration in your docker-compose.yml:
app:
image: laravel-app
restart: unless-stopped
env_file:
- .env
environment:
PHP_OPCACHE_ENABLE: '1'
AUTORUN_ENABLED: 'true'
HEALTHCHECK_PATH: '/up'
volumes:
- storage:/var/www/html/storage
- bootstrap-cache:/var/www/html/bootstrap/cache
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
command: ['php', 'artisan', 'octane:start', '--server=frankenphp', '--host=0.0.0.0', '--port=8080']
healthcheck:
test: ['CMD', 'healthcheck-octane']
start_period: 10s
labels:
- "traefik.enable=true"
- "traefik.http.routers.testing-laravel.rule=Host(`${APP_HOST}`)"
- "traefik.http.routers.testing-laravel.entrypoints=websecure"
- "traefik.http.routers.testing-laravel.tls=true"
- "traefik.http.services.testing-laravel.loadbalancer.server.port=8080"
networks:
- laravel
- traefik-proxy
The key differences are the custom command to start Octane with FrankenPHP and the healthcheck-octane health check command.
Testing and Verification
Build and start your containers:
docker-compose build
docker-compose up -d
Verify all services are running:
docker-compose ps
Check the logs for any errors:
docker-compose logs -f app
Access your application at the configured APP_HOST domain. If using Traefik's default local development setup, navigate to http://laravel-app.docker.localhost.
Common Troubleshooting
- Permission errors: Ensure all files are owned by
www-datainside containers - Database connection failures: Verify MySQL is healthy before app starts (
depends_onwith health checks) - Asset compilation failures: Check that Node.js and npm are properly installed in the Dockerfile
- WebSocket connection issues: Confirm Reverb service has proper Traefik labels and network access
- Trusted proxy errors: Verify you've configured trusted proxies as described in the configuration section
Conclusion
You now have a production-ready Laravel deployment using Docker with all essential services configured. This setup provides automatic health checks, graceful shutdowns, and isolated service management.
Refer to the serversideup/php documentation for advanced configuration options and the Traefik documentation for load balancing and advanced routing scenarios.