A production Laravel application on a 4 vCPU VPS handles 800-1,200 requests per second with proper PHP-FPM tuning, OPcache configuration, and Redis queue management. This guide covers every configuration file needed to run Laravel in production, from nginx virtual hosts to Envoy zero-downtime deployment scripts.
All configurations are tested on Ubuntu 22.04 with PHP 8.3, Laravel 11, and Redis 7. For VPS provisioning, check available plans on the Pricing page.
PHP-FPM Process Manager Configuration
PHP-FPM is the process manager that handles incoming PHP requests. The default configuration is designed for shared hosting with minimal resources. A VPS needs a tuned configuration that matches your available RAM and expected traffic patterns.
There are three process management modes:
- static: Fixed number of worker processes. Best for predictable traffic with consistent RAM availability.
- dynamic: Workers scale between a minimum and maximum. Best for variable traffic patterns.
- ondemand: Workers spawn only when requests arrive and die after idle timeout. Best for low-traffic sites sharing resources.
For a production Laravel application on a 4 vCPU, 8GB RAM VPS:
; /etc/php/8.3/fpm/pool.d/laravel.conf
[laravel]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm-laravel.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; Process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
pm.process_idle_timeout = 10s
; Status page for monitoring
pm.status_path = /fpm-status
ping.path = /fpm-ping
; Logging
access.log = /var/log/php-fpm/laravel-access.log
slowlog = /var/log/php-fpm/laravel-slow.log
request_slowlog_timeout = 5s
; Environment variables
env[APP_ENV] = production
env[APP_DEBUG] = false
; PHP settings overrides
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 60
php_admin_value[upload_max_filesize] = 50M
php_admin_value[post_max_size] = 50M
The `pm.max_children = 50` setting is calculated from available RAM. Each PHP-FPM worker uses approximately 40-80MB of memory for a Laravel application. With 8GB RAM, after accounting for the OS (1GB), PostgreSQL (2GB), Redis (512MB), and nginx (256MB), roughly 4GB is available for PHP workers. At 80MB per worker, that is 50 workers maximum.
The `pm.max_requests = 1000` setting recycles workers after 1,000 requests, preventing memory leaks from accumulating. This is essential for long-running Laravel applications.
OPcache Configuration
OPcache stores compiled PHP bytecode in shared memory, eliminating the need to parse and compile PHP files on every request. For Laravel, this reduces response time by 50-70% because the framework bootstraps thousands of files per request.
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.save_comments=1
opcache.enable_file_override=1
opcache.jit=1255
opcache.jit_buffer_size=128M
Critical setting: `opcache.validate_timestamps=0` disables file modification checks in production. This means OPcache never checks if PHP files have changed, giving maximum performance. The tradeoff is that you must restart PHP-FPM after every deployment to load new code. This is handled automatically in the Envoy deployment script below.
The JIT compiler (`opcache.jit=1255`) provides an additional 10-30% performance improvement for CPU-intensive operations like JSON serialization, collection manipulation, and string processing. The `1255` setting enables optimized JIT tracing, which is the best mode for web applications.
Redis Configuration for Laravel
Laravel uses Redis for three distinct purposes: cache store, session driver, and queue backend. Each should use a separate Redis database to allow independent flushing and monitoring.
// config/database.php - Redis connections
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0, // Application cache
'prefix' => 'cache:',
],
'session' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 1, // Session storage
'prefix' => 'session:',
],
'queue' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 2, // Queue jobs
'prefix' => 'queue:',
],
],
Using `phpredis` extension instead of `predis` (PHP library) gives 2-3x better performance on high-throughput workloads because it is a compiled C extension. Install it with `pecl install redis` and add `extension=redis.so` to your PHP configuration.
Laravel Horizon for Queue Management
Horizon provides a dashboard and configuration layer for Laravel queues. It monitors job throughput, failure rates, and queue wait times. Configure it for production workloads:
// config/horizon.php
'environments' => [
'production' => [
'supervisor-default' => [
'connection' => 'redis',
'queue' => ['default', 'emails', 'notifications'],
'balance' => 'auto',
'minProcesses' => 3,
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
'tries' => 3,
'timeout' => 300,
'maxTime' => 3600,
'memory' => 128,
'nice' => 0,
],
'supervisor-heavy' => [
'connection' => 'redis',
'queue' => ['reports', 'exports', 'imports'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 5,
'tries' => 1,
'timeout' => 1800,
'maxTime' => 3600,
'memory' => 512,
],
],
],
The `balance => 'auto'` setting lets Horizon automatically distribute workers across queues based on workload. During peak email sending periods, it shifts workers to the email queue. During report generation, workers move to the heavy queue. This eliminates manual worker allocation.
Nginx Virtual Host Configuration
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name app.example.com;
root /var/www/app/current/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Asset caching
location /build/ {
alias /var/www/app/current/public/build/;
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
}
location /storage/ {
alias /var/www/app/shared/storage/app/public/;
expires 30d;
add_header Cache-Control "public";
}
# PHP processing via FPM socket
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm-laravel.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_read_timeout 300;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Block dotfiles except .well-known
location ~ /\.(?!well-known) {
deny all;
}
}
The `root` points to `/var/www/app/current/public` where `current` is a symlink managed by the Envoy deployment script. This enables zero-downtime deployments by switching the symlink to a new release directory.
Envoy Zero-Downtime Deployment Script
Laravel Envoy provides a simple syntax for defining deployment tasks. This script implements zero-downtime deployment with rollback capability:
<h1>Envoy.blade.php</h1>
@servers(['production' => 'deploy@your-server-ip'])
@setup
$repository = '[email protected]:your-org/your-app.git';
$releases_dir = '/var/www/app/releases';
$shared_dir = '/var/www/app/shared';
$current_dir = '/var/www/app/current';
$release = date('YmdHis');
$new_release_dir = $releases_dir . '/' . $release;
@endsetup
@story('deploy')
clone_repository
install_dependencies
build_assets
update_symlinks
optimize_application
restart_services
cleanup_old_releases
@endstory
@task('clone_repository')
echo "Cloning repository..."
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
@endtask
@task('install_dependencies')
echo "Installing Composer dependencies..."
cd {{ $new_release_dir }}
composer install --no-dev --optimize-autoloader --no-interaction
@endtask
@task('build_assets')
echo "Building frontend assets..."
cd {{ $new_release_dir }}
npm ci && npm run build
@endtask
@task('update_symlinks')
echo "Updating symlinks..."
ln -nfs {{ $shared_dir }}/.env {{ $new_release_dir }}/.env
ln -nfs {{ $shared_dir }}/storage {{ $new_release_dir }}/storage
ln -nfs {{ $new_release_dir }} {{ $current_dir }}
@endtask
@task('optimize_application')
echo "Optimizing Laravel..."
cd {{ $current_dir }}
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan migrate --force
@endtask
@task('restart_services')
echo "Restarting services..."
sudo systemctl reload php8.3-fpm
sudo supervisorctl restart horizon
@endtask
@task('cleanup_old_releases')
echo "Cleaning old releases..."
cd {{ $releases_dir }}
ls -dt */
tail -n +6
xargs rm -rf
@endtask
Deploy with a single command: `envoy run deploy`. The script clones the new release, installs dependencies, builds assets, switches the symlink, and restarts PHP-FPM. The old release remains on disk for instant rollback. Only the last 5 releases are kept to conserve disk space.
For managed deployment pipelines that handle this automatically, explore the Services Overview page.
Supervisor Configuration for Queue Workers
; /etc/supervisor/conf.d/horizon.conf
[program:horizon]
process_name=%(program_name)s
command=php /var/www/app/current/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/horizon.log
stopwaitsecs=3600
stopsignal=SIGTERM
The `stopwaitsecs=3600` setting gives Horizon up to 1 hour to finish processing the current job before the supervisor forces a kill. This prevents data corruption in long-running jobs like report generation or CSV exports.
Laravel Performance Checklist
Run through this checklist before going to production:
| Item | Command | Impact |
| Config cache | `php artisan config:cache` | 30% faster boot |
| Route cache | `php artisan route:cache` | 50% faster routing |
| View cache | `php artisan view:cache` | 20% faster rendering |
| Event cache | `php artisan event:cache` | 10% faster events |
| Composer autoload | `composer dump-autoload -o` | 15% faster class loading |
| Debug off | `APP_DEBUG=false` | Prevents info leaks |
| Query logging off | `DB_LOG_QUERIES=false` | 5-10% less memory |
| Telescope disabled | Remove from production providers | 20% less overhead |
Combined, these optimizations improve Laravel response time by 40-60% compared to a default development configuration.
Read the Documentation for additional PHP hosting optimization guides and deployment patterns.
Frequently Asked Questions
Q: How many PHP-FPM workers should I run?
A: Divide available RAM (after OS, database, Redis, nginx) by the average memory per worker (40-80MB for Laravel). A 4GB VPS typically runs 25-50 workers. Monitor with `pm.status_path` and adjust based on actual memory usage.
Q: Should I use nginx or Apache for Laravel?
A: Nginx. It handles 2-5x more concurrent connections with 50% less memory. Apache with mod_php is simpler to configure but cannot match nginx + PHP-FPM performance at scale. Every serious Laravel deployment uses nginx.
Q: How do I handle Laravel scheduled tasks on a VPS?
A: Add a single cron entry: `* * * * * cd /var/www/app/current && php artisan schedule:run >> /dev/null 2>&1`. Laravel's scheduler handles all task timing internally. Do not create individual cron entries for each task.
Q: What is the best session driver for production?
A: Redis. File sessions create I/O bottlenecks under load and cannot be shared across multiple servers. Database sessions add unnecessary query load. Redis sessions are in-memory, fast, and automatically expire. Configure with `SESSION_DRIVER=redis` in your `.env` file.
Q: How do I monitor PHP-FPM performance?
A: Enable the status page with `pm.status_path = /fpm-status` in your pool configuration. Monitor `active processes`, `idle processes`, `listen queue`, and `max children reached` metrics. If `max children reached` is greater than 0, increase `pm.max_children` or add more RAM.
Q: Can I run multiple Laravel applications on one VPS?
A: Yes. Create separate PHP-FPM pools (separate `.conf` files in `/etc/php/8.3/fpm/pool.d/`) with different socket paths, and configure separate nginx server blocks pointing to each pool. Each application gets isolated processes and memory limits. Contact us through the Contact page for multi-application hosting architecture advice.