Laravel Complete Setup
Laravel Complete Setup
Production-ready Laravel configuration with all services: PHP-FPM, Nginx, Horizon, Reverb, queue workers, and Laravel Scheduler.
Use Cases
- ✅ Complete Laravel production deployments
- ✅ Multi-service orchestration with dependencies
- ✅ Queue processing with Horizon dashboard
- ✅ Real-time features with Reverb WebSockets
- ✅ Scheduled tasks with Laravel Scheduler
- ✅ Health monitoring and metrics
Complete Configuration
File: cbox-init.yaml
version: "1.0"
global:
shutdown_timeout: 60
log_format: json
log_level: info
metrics_enabled: true
metrics_port: 9090
# Laravel optimization hooks
hooks:
pre-start:
- name: config-cache
command: ["php", "artisan", "config:cache"]
timeout: 60
- name: route-cache
command: ["php", "artisan", "route:cache"]
timeout: 60
- name: view-cache
command: ["php", "artisan", "view:cache"]
timeout: 120
- name: migrate
command: ["php", "artisan", "migrate", "--force"]
timeout: 300
- name: storage-link
command: ["php", "artisan", "storage:link"]
timeout: 30
continue_on_error: true
processes:
# PHP-FPM - Core application runtime
php-fpm:
enabled: true
command: ["php-fpm", "-F", "-R"]
restart: always
health_check:
type: tcp
address: 127.0.0.1:9000
initial_delay: 5
period: 10
timeout: 3
failure_threshold: 3
shutdown:
signal: SIGQUIT
timeout: 30
# Nginx - Web server
nginx:
enabled: true
command: ["nginx", "-g", "daemon off;"]
restart: always
depends_on: [php-fpm]
health_check:
type: http
url: http://localhost/health
initial_delay: 3
period: 10
timeout: 2
failure_threshold: 3
expected_status: 200
shutdown:
signal: SIGTERM
timeout: 30
# Laravel Horizon - Queue dashboard + workers
horizon:
enabled: true
command: ["php", "artisan", "horizon"]
restart: always
depends_on: [php-fpm]
health_check:
type: exec
command: ["php", "artisan", "horizon:status"]
initial_delay: 10
period: 30
timeout: 5
failure_threshold: 2
shutdown:
pre_stop_hook:
name: horizon-terminate
command: ["php", "artisan", "horizon:terminate"]
timeout: 60
signal: SIGTERM
timeout: 120
# Laravel Reverb - WebSocket server
reverb:
enabled: false # Enable for real-time features
command: ["php", "artisan", "reverb:start"]
restart: always
depends_on: [php-fpm]
health_check:
type: tcp
address: 127.0.0.1:8080
initial_delay: 5
period: 15
timeout: 3
shutdown:
pre_stop_hook:
name: reverb-restart
command: ["php", "artisan", "reverb:restart"]
timeout: 30
signal: SIGTERM
timeout: 60
# Queue workers - Default queue
queue-default:
enabled: true
command: ["php", "artisan", "queue:work", "--queue=default", "--tries=3", "--max-time=3600"]
scale: 2
restart: on-failure
depends_on: [php-fpm]
shutdown:
signal: SIGTERM
timeout: 60
# Laravel Scheduler - Cron replacement
scheduler:
enabled: true
command: ["php", "artisan", "schedule:run"]
schedule: "* * * * *" # Every minute
restart: never
depends_on: [php-fpm]
Architecture Overview
┌─────────────────────────────────────────────────┐
│ Container (Cbox Init as PID 1) │
│ │
│ Pre-Start Hooks: │
│ 1. config:cache ─> 2. route:cache ─> │
│ 3. view:cache ─> 4. migrate ─> 5. storage │
│ │
│ ┌─────────────┐ │
│ │ PHP-FPM │ (Priority 10, TCP health) │
│ │ :9000 │ │
│ └──────┬──────┘ │
│ │ │
│ ├──> ┌─────────────┐ │
│ │ │ Nginx │ (Priority 20) │
│ │ │ :80 │ (HTTP health) │
│ │ └─────────────┘ │
│ │ │
│ ├──> ┌─────────────┐ │
│ │ │ Horizon │ (Priority 30) │
│ │ │ │ (Exec health) │
│ │ └─────────────┘ │
│ │ │
│ ├──> ┌─────────────┐ │
│ │ │ Reverb │ (Priority 30) │
│ │ │ :8080 │ (TCP health) │
│ │ └─────────────┘ │
│ │ │
│ └──> ┌─────────────┐ │
│ │ Queue ×2 │ (Priority 40) │
│ │ Workers │ (Scaled) │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ Scheduler │ (Cron: * * * * *) │
│ └─────────────┘ │
└─────────────────────────────────────────────────┘
Configuration Walkthrough
Pre-Start Hooks
Optimize Laravel before starting processes:
hooks:
pre-start:
# Cache configuration files
- name: config-cache
command: ["php", "artisan", "config:cache"]
timeout: 60
# Cache routes
- name: route-cache
command: ["php", "artisan", "route:cache"]
timeout: 60
# Cache Blade views
- name: view-cache
command: ["php", "artisan", "view:cache"]
timeout: 120
# Run database migrations
- name: migrate
command: ["php", "artisan", "migrate", "--force"]
timeout: 300
# Create storage symlink
- name: storage-link
command: ["php", "artisan", "storage:link"]
timeout: 30
continue_on_error: true # OK if already exists
Why these hooks:
- config:cache - Speeds up config loading by 2-3x
- route:cache - Dramatically faster routing (10-50x)
- view:cache - Pre-compile Blade templates
- migrate - Ensure database schema is current
- storage:link - Create public storage symlink
PHP-FPM Configuration
php-fpm:
enabled: true
command: ["php-fpm", "-F", "-R"]
restart: always
health_check:
type: tcp
address: 127.0.0.1:9000 # PHP-FPM FastCGI port
initial_delay: 5
period: 10
timeout: 3
failure_threshold: 3
shutdown:
signal: SIGQUIT # Graceful shutdown for PHP-FPM
timeout: 30
Key points:
- TCP health check on port 9000 (FastCGI)
- SIGQUIT for graceful shutdown (vs SIGTERM)
- 30-second timeout to finish active requests
Nginx Configuration
nginx:
enabled: true
command: ["nginx", "-g", "daemon off;"]
depends_on: [php-fpm] # Wait for PHP-FPM health
health_check:
type: http
url: http://localhost/health
expected_status: 200
period: 10
Dependency behavior:
- Waits for PHP-FPM to pass health checks
- Prevents "502 Bad Gateway" errors on startup
- Ensures FastCGI backend is ready
Health endpoint:
// routes/web.php
Route::get('/health', function () {
return response()->json(['status' => 'healthy'], 200);
});
Laravel Horizon Configuration
horizon:
enabled: true
command: ["php", "artisan", "horizon"]
depends_on: [php-fpm]
shutdown:
pre_stop_hook:
command: ["php", "artisan", "horizon:terminate"]
timeout: 60 # Wait for terminate signal
timeout: 120 # Total shutdown time (finish jobs)
Graceful shutdown flow:
horizon:terminatesends termination signal- Horizon stops accepting new jobs
- Currently running jobs finish
- Horizon exits cleanly
- If timeout (120s) expires, SIGTERM is sent
Queue Worker Configuration
queue-default:
enabled: true
command: ["php", "artisan", "queue:work", "--queue=default", "--tries=3", "--max-time=3600"]
scale: 2 # Run 2 workers
restart: on-failure # Only restart on errors
depends_on: [php-fpm]
shutdown:
signal: SIGTERM
timeout: 60 # Finish current job
Worker arguments:
--queue=default- Process default queue--tries=3- Retry failed jobs 3 times--max-time=3600- Restart worker after 1 hour
Scaling:
scale: 2createsqueue-default-1andqueue-default-2- Adjust based on queue depth and processing time
- Typical range: 2-10 workers
Laravel Scheduler
scheduler:
enabled: true
command: ["php", "artisan", "schedule:run"]
schedule: "* * * * *" # Every minute
restart: never
How it works:
- Runs
php artisan schedule:runevery minute - Laravel's internal scheduler handles task timing
- Replaces cron in containers
Heartbeat monitoring:
scheduler:
heartbeat:
failure_url: https://hc-ping.com/uuid/fail
Startup Sequence
1. Pre-Start Hooks (sequential)
└─> config:cache → route:cache → view:cache → migrate → storage:link
2. PHP-FPM (priority 10)
└─> Waits for TCP health check on :9000
3. Nginx (priority 20)
└─> Waits for PHP-FPM health, then starts
└─> HTTP health check on :80/health
4. Horizon + Reverb (priority 30, parallel)
└─> Both wait for PHP-FPM health
└─> Start simultaneously
5. Queue Workers (priority 40)
└─> Waits for PHP-FPM health
└─> 2 instances start: queue-default-1, queue-default-2
6. Scheduler (priority 50)
└─> Waits for PHP-FPM health
└─> Runs every minute via cron schedule
Dockerfile Integration
FROM php:8.3-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
nginx \
supervisor \
curl
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql opcache
# Copy application
WORKDIR /var/www/html
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Copy Cbox Init
COPY --from=ghcr.io/cboxdk/init:latest /cbox-init /usr/local/bin/cbox-init
# Copy configuration
COPY cbox-init.yaml /etc/cbox-init/cbox-init.yaml
# Copy Nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Expose ports
EXPOSE 80 9090
# Environment defaults
ENV PHP_FPM_AUTOTUNE_PROFILE=medium \
CBOX_INIT_GLOBAL_LOG_FORMAT=json \
CBOX_INIT_GLOBAL_METRICS_ENABLED=true
# Run Cbox Init as PID 1
ENTRYPOINT ["/usr/local/bin/cbox-init"]
Nginx Configuration
File: nginx.conf
worker_processes auto;
error_log /dev/stderr warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format json escape=json
'{'
'"time":"$time_iso8601",'
'"request":"$request",'
'"status":$status,'
'"bytes":$body_bytes_sent,'
'"duration":$request_time,'
'"ip":"$remote_addr"'
'}';
access_log /dev/stdout json;
sendfile on;
keepalive_timeout 65;
upstream php-fpm {
server 127.0.0.1:9000;
}
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php-fpm;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
Running the Stack
Build and Run
# Build Docker image
docker build -t laravel-app:latest .
# Run with auto-tuning
docker run -d \
--name laravel \
-p 80:80 \
-p 9090:9090 \
-e PHP_FPM_AUTOTUNE_PROFILE=medium \
-e DB_HOST=host.docker.internal \
-m 2G \
--cpus 2 \
laravel-app:latest
Docker Compose
version: '3.8'
services:
app:
build: .
ports:
- "80:80"
- "9090:9090" # Prometheus metrics
environment:
PHP_FPM_AUTOTUNE_PROFILE: "medium"
CBOX_INIT_GLOBAL_METRICS_ENABLED: "true"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "3"
# Laravel environment
APP_ENV: production
APP_KEY: ${APP_KEY}
DB_CONNECTION: mysql
DB_HOST: database
REDIS_HOST: redis
deploy:
resources:
limits:
memory: 2G
cpus: '2'
depends_on:
- database
- redis
database:
image: mysql:8.0
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
volumes:
- db-data:/var/lib/mysql
redis:
image: redis:alpine
volumes:
db-data:
Service Breakdown
1. PHP-FPM (Priority 10)
Purpose: Core PHP runtime for handling web requests
Configuration:
php-fpm:
command: ["php-fpm", "-F", "-R"] # Foreground, allow root
health_check:
type: tcp
address: 127.0.0.1:9000 # FastCGI port
Auto-Tuning:
# With 2GB container and medium profile
# Calculated: ~16 workers (CPU limited)
PHP_FPM_AUTOTUNE_PROFILE=medium
See PHP-FPM Auto-Tuning for worker optimization.
2. Nginx (Priority 20)
Purpose: Web server, reverse proxy to PHP-FPM
Configuration:
nginx:
depends_on: [php-fpm] # Wait for PHP-FPM
health_check:
type: http
url: http://localhost/health
expected_status: 200
Why depends_on:
- Prevents "502 Bad Gateway" during startup
- Nginx starts only after PHP-FPM is healthy
- Avoids race conditions
3. Laravel Horizon (Priority 30)
Purpose: Queue dashboard and supervised queue workers
Configuration:
horizon:
shutdown:
pre_stop_hook:
command: ["php", "artisan", "horizon:terminate"]
timeout: 60
timeout: 120 # Total shutdown time
Graceful shutdown:
- Sends terminate signal via Artisan command
- Allows current jobs to complete
- Maximum 120 seconds before force-kill
Access dashboard:
http://your-app.com/horizon
4. Laravel Reverb (Priority 30)
Purpose: Real-time WebSocket server for broadcasting
When to enable:
reverb:
enabled: true # Enable for:
# - Real-time notifications
# - Live chat features
# - Collaborative editing
# - Live dashboards
Configuration:
// config/broadcasting.php
'reverb' => [
'driver' => 'reverb',
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
],
5. Queue Workers (Priority 40)
Purpose: Background job processing
Scaling:
queue-default:
scale: 2 # Creates queue-default-1, queue-default-2
Adjust scale based on:
- Queue depth (jobs waiting)
- Job processing time
- Available resources
Monitoring:
# Via Horizon dashboard
http://your-app.com/horizon
# Via Prometheus metrics
curl http://localhost:9090/metrics | grep queue
6. Laravel Scheduler (Priority 50)
Purpose: Run scheduled tasks (replaces cron)
Configuration:
scheduler:
command: ["php", "artisan", "schedule:run"]
schedule: "* * * * *" # Every minute
How it works:
- Runs every minute via cron schedule
- Laravel determines which tasks to execute
- Tasks defined in
app/Console/Kernel.php
Example Kernel:
protected function schedule(Schedule $schedule)
{
$schedule->command('backup:run')->daily();
$schedule->command('emails:send')->hourly();
}
Environment Configuration
Development
# .env.development
PHP_FPM_AUTOTUNE_PROFILE=dev
CBOX_INIT_GLOBAL_LOG_LEVEL=debug
CBOX_INIT_PROCESS_HORIZON_ENABLED=false
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE=1
CBOX_INIT_PROCESS_SCHEDULER_ENABLED=false
Staging
# .env.staging
PHP_FPM_AUTOTUNE_PROFILE=medium
CBOX_INIT_GLOBAL_LOG_LEVEL=info
CBOX_INIT_GLOBAL_METRICS_ENABLED=true
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE=3
Production
# .env.production
PHP_FPM_AUTOTUNE_PROFILE=heavy
CBOX_INIT_GLOBAL_LOG_LEVEL=warn
CBOX_INIT_GLOBAL_LOG_REDACTION_ENABLED=true
CBOX_INIT_GLOBAL_METRICS_ENABLED=true
CBOX_INIT_GLOBAL_API_ENABLED=true
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE=10
Monitoring
Prometheus Metrics
# Check metrics endpoint
curl http://localhost:9090/metrics
# Key metrics:
# - cbox_init_process_up{process="php-fpm"}
# - cbox_init_process_restarts_total{process="nginx"}
# - cbox_init_process_health_status{process="horizon"}
Health Status
# Check all process health
curl http://localhost:9180/api/v1/processes | jq '.[] | {name, health_status, state}'
Troubleshooting
Nginx 502 Bad Gateway
Symptom: Nginx starts before PHP-FPM is ready
Solution: Already configured with depends_on: [php-fpm]
Verify:
# Check startup order in logs
docker logs laravel-app | grep "started successfully"
# Should see:
# php-fpm started successfully
# nginx started successfully (after php-fpm)
Horizon Won't Terminate
Symptom: Horizon doesn't stop gracefully, gets force-killed
Solution: Increase shutdown timeout
horizon:
shutdown:
timeout: 300 # 5 minutes for long-running jobs
Queue Workers Restarting
Symptom: Queue workers restart frequently
Solution: Check memory usage
queue-default:
command: ["php", "artisan", "queue:work", "--max-jobs=100"] # Restart after 100 jobs
Or increase container memory:
docker run -m 4G laravel-app # Was 2G
Migrations Timeout
Symptom: Pre-start migrate hook times out
Solution: Increase timeout
hooks:
pre-start:
- name: migrate
timeout: 600 # 10 minutes for large migrations
Performance Tuning
PHP-FPM Workers
# Optimize based on container size
PHP_FPM_AUTOTUNE_PROFILE=medium # 2GB container
PHP_FPM_AUTOTUNE_PROFILE=heavy # 4-8GB container
Queue Worker Count
# Start with 1 worker per CPU core
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE=2 # 2 CPUs
# Scale up based on queue depth
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE=5 # High traffic
Horizon vs Queue Workers
Use Horizon when:
- Need queue dashboard and monitoring
- Want auto-balancing across queues
- Managing complex queue configurations
Use queue:work when:
- Simple queue processing
- Lower resource overhead
- Fine-grained control per queue
See Also
- PHP-FPM Auto-Tuning - Worker optimization
- Health Checks - Health monitoring
- Lifecycle Hooks - Pre/post hooks
- Process Scaling - Scale workers dynamically
- Prometheus Metrics - Monitoring guide