The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on OVH for Laravel
Nginx as a High-Performance Frontend for Laravel
When deploying Laravel applications, Nginx serves as an excellent choice for a web server due to its asynchronous, event-driven architecture, making it highly efficient for handling concurrent connections. For a Laravel application, Nginx’s primary roles are serving static assets, acting as a reverse proxy to your PHP application server (Gunicorn or PHP-FPM), and handling SSL termination.
Optimizing Nginx Configuration
A well-tuned Nginx configuration is crucial. We’ll focus on key directives that impact performance and stability. The following configuration snippet assumes you are using PHP-FPM for your Laravel application. If you are using Gunicorn with a WSGI adapter like Gunicorn-WSGI, the `fastcgi_pass` directive would be replaced with `proxy_pass` to your Gunicorn instance.
Core Nginx Directives for Laravel
These directives are typically placed within the http block or a specific server block.
Worker Processes and Connections
worker_processes should ideally be set to the number of CPU cores available on your server. worker_connections defines the maximum number of simultaneous connections that each worker process can handle. The total maximum connections will be worker_processes * worker_connections.
worker_processes auto; # Or set to the number of CPU cores
events {
worker_connections 4096; # Adjust based on server resources and expected load
multi_accept on;
}
HTTP Block Optimizations
Directives within the http block affect all server blocks. Enabling Gzip compression significantly reduces bandwidth usage and improves load times. Setting a reasonable client_body_buffer_size and client_max_body_size is important for handling file uploads.
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip Compression
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Buffers
client_body_buffer_size 10M; # Adjust as needed for large uploads
client_max_body_size 10M; # Adjust as needed for large uploads
large_client_header_buffers 4 16k;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# Include server blocks
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Laravel Server Block Configuration
This is a typical server block for a Laravel application using PHP-FPM. Key elements include caching for static assets, proper handling of PHP requests, and security headers.
server {
listen 80;
listen [::]:80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_laravel_app/public; # Adjust to your Laravel public directory
index index.php index.html index.htm;
# SSL Configuration (Recommended)
# listen 443 ssl http2;
# listen [::]:443 ssl http2;
# server_name your_domain.com www.your_domain.com;
# ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
# include /etc/letsencrypt/options-ssl-nginx.conf;
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:;"; # Customize CSP as needed
# Static Asset Caching
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri $uri/ =404;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Handle all other requests to Laravel's front controller
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass PHP scripts to PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Adjust fastcgi_pass to your PHP-FPM socket or IP:Port
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Example for PHP 8.1
# fastcgi_pass 127.0.0.1:9000; # Example for TCP/IP
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~* (composer\.json|composer\.lock|\.env|\.git|\.env.example) {
deny all;
}
# Error pages
error_page 404 /index.php; # Or a custom 404 page
error_page 500 502 503 504 /index.php; # Or custom error handling
}
Troubleshooting Nginx Performance
If you encounter performance issues, start by examining Nginx’s error logs (/var/log/nginx/error.log) and access logs. Use tools like ab (ApacheBench) or wrk for load testing, and strace or tcpdump for deep network analysis. Monitor server resource utilization (CPU, memory, I/O) using htop, vmstat, and iostat.
Gunicorn/PHP-FPM Tuning for Laravel
The application server is where your PHP code is executed. For PHP applications, this is typically PHP-FPM. If you’re using a Python-based framework, Gunicorn is a common choice. This section focuses on PHP-FPM, but the principles of worker management and resource allocation apply broadly.
PHP-FPM Configuration Tuning
PHP-FPM’s configuration file (often /etc/php/X.Y/fpm/php-fpm.conf and pool configurations in /etc/php/X.Y/fpm/pool.d/www.conf) is critical for performance. The pm (process manager) settings are particularly important.
Process Manager (pm) Settings
PHP-FPM offers three process manager modes:
static: A fixed number of child processes are spawned when the master process starts and remain alive. Good for predictable loads.dynamic: The number of child processes varies based on demand, within defined limits.ondemand: Processes are spawned only when a request arrives and are killed after a period of inactivity.
For most Laravel applications, dynamic offers a good balance between responsiveness and resource utilization. Tuning pm.max_children, pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers is key.
; /etc/php/X.Y/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock # Match this with Nginx config listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 100 ; Maximum number of children that can be alive at the same time. pm.start_servers = 10 ; Number of children created at startup. pm.min_spare_servers = 5 ; Number of children that should be kept idle. pm.max_spare_servers = 20 ; Maximum number of children that can be idle. pm.process_idle_timeout = 10s; The timeout for a child process to become idle. ; If using pm = static ; pm.max_children = 50 ; If using pm = ondemand ; pm.max_children = 50 ; pm.process_idle_timeout = 10s ; Timeout for ondemand processes ; Request handling request_terminate_timeout = 60s ; Timeout for script execution ; request_slowlog_timeout = 10s ; Log slow requests (useful for debugging) ; slowlog = /var/log/php/php-fpm_slow.log ; Memory limits ; memory_limit = 256M ; Adjust based on your application's needs ; max_execution_time = 60 ; max_input_vars = 1000
Tuning Strategy:
- Start with a conservative
pm.max_children(e.g., 50-100) and monitor CPU and memory usage. - Increase
pm.max_childrenif you see requests queuing up or workers consistently maxed out, but be mindful of total RAM. A common formula is(Total RAM - RAM for OS/Nginx) / Average RAM per PHP-FPM worker. - Adjust
pm.start_serversto ensure enough workers are ready for initial traffic spikes. pm.min_spare_serversandpm.max_spare_serverscontrol the pool of idle workers. Higher values can improve responsiveness but consume more memory.request_terminate_timeoutshould be set to a reasonable value to prevent runaway scripts from hogging resources, but not so low that legitimate long-running tasks fail.
PHP Configuration (php.ini)
Beyond PHP-FPM settings, global PHP directives in php.ini also impact performance. Key settings include:
; /etc/php/X.Y/fpm/php.ini memory_limit = 256M ; Max memory a script can consume max_execution_time = 60 ; Max execution time for a script max_input_vars = 3000 ; Max input variables (useful for forms with many fields) post_max_size = 10M ; Max size of POST data (should align with Nginx's client_max_body_size) upload_max_filesize = 10M ; Max size of an uploaded file (should align with Nginx's client_max_body_size) realpath_cache_size = 4096k ; Size of the realpath cache (improves file lookup performance) realpath_cache_ttl = 600 ; Time-to-live for the realpath cache entries opcache.enable=1 opcache.memory_consumption=128 ; MB opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.revalidate_freq=2 opcache.validate_timestamps=0 ; Set to 1 in development, 0 in production for max performance opcache.enable_cli=1
OPcache is essential for PHP performance. Ensure it’s enabled and configured with sufficient memory. Setting opcache.validate_timestamps=0 in production disables file timestamp checking, providing the fastest performance but requiring a cache clear or server restart after code deployments.
Troubleshooting PHP-FPM Performance
Monitor PHP-FPM logs (often in /var/log/php/). Use strace on PHP-FPM worker processes to diagnose hangs or resource contention. Profiling tools like Xdebug (in development) or Blackfire.io can pinpoint slow code execution. Check the pm.status_path in your pool configuration (e.g., /status) to see real-time worker status.
MongoDB Performance Tuning on OVH
MongoDB’s performance is heavily influenced by hardware, configuration, and query patterns. OVH’s dedicated servers offer excellent raw performance, but proper MongoDB tuning is still required.
Key MongoDB Configuration Directives
The main configuration file is typically /etc/mongod.conf. Focus on:
# /etc/mongod.conf
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger # Recommended storage engine
# network interfaces
net:
port: 27017
bindIp: 0.0.0.0 # Or specific IPs for security
# security:
# authorization: enabled
# logging:
# quiet: true
# path: /var/log/mongodb/mongod.log
# logAppend: true
# verbosity: 0
# operationProfiling:
# mode: slowOp
# slowOpThresholdMs: 100
# Sharding (if applicable)
# sharding:
# clusterRole: configsvr
# configsvrFilePermissions:
# idleInterval: 1000
# maxTimeMS: 5000
# minSnapshotCount: 2
# snapshotTimeout: 1000
# syncFrom: config1.example.net
# Replication (if applicable)
# replication:
# replSetName: rs0
WiredTiger Storage Engine Optimizations
WiredTiger is the default and recommended storage engine. Its performance is heavily influenced by its cache size.
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger
wiredTiger:
collectionConfig:
cacheSizeGB: 0.75 # Example: 75% of available RAM for cache, adjust based on server RAM
engineConfig:
cacheSizeGB: 0.75 # Example: 75% of available RAM for cache, adjust based on server RAM
# concurrentReadTransactions: 16 # Adjust based on read concurrency needs
# concurrentWriteTransactions: 16 # Adjust based on write concurrency needs
Cache Size: The storage.wiredTiger.engineConfig.cacheSizeGB (and collectionConfig for per-collection tuning) is critical. A common recommendation is to allocate 50-75% of the server’s RAM to the WiredTiger cache. Monitor cache hit rates using MongoDB’s diagnostic commands.
Indexing Strategy for Laravel Applications
Proper indexing is paramount for MongoDB performance, especially with Laravel applications that might perform complex queries. Analyze your application’s query patterns using the slowOpThresholdMs setting in mongod.conf and the db.slowQueries.find() cursor.
Use the explain() method on your queries to understand how they are executed and identify missing indexes.
// Example: Analyzing a find query
db.users.find({ "email": "[email protected]" }).explain("executionStats")
// Example: Creating an index
db.users.createIndex({ "email": 1 }) // 1 for ascending, -1 for descending
db.posts.createIndex({ "author_id": 1, "created_at": -1 }) // Compound index
For Laravel Eloquent, indexes should align with the fields used in where() clauses, orderBy(), and relationships.
Monitoring and Diagnostics
Regular monitoring is key. Use MongoDB’s built-in commands and external tools:
db.serverStatus(): Provides a wealth of information about the server’s current state, including connections, operations, memory usage, and cache statistics.db.stats(): Shows database-level statistics, including data size, index size, and object counts.db.collection.stats(): Provides statistics for a specific collection.mongotop: Shows real-time read/write activity for collections.mongostat: Provides a high-level overview of server performance metrics.- OVH Control Panel: Monitor CPU, RAM, disk I/O, and network traffic for your dedicated server.
Pay close attention to the WiredTiger cache hit rate. A consistently low hit rate (below 90%) suggests the cache is too small or queries are not well-indexed.
OVH Specific Considerations
OVH dedicated servers often come with high-performance NVMe SSDs. Ensure your MongoDB data directory is on the fastest available storage. For production environments, consider RAID configurations for redundancy and performance, though NVMe drives are increasingly reliable on their own. Network latency between your application server and the database server (if separate) is also critical; keep them geographically close or on the same high-speed internal network.