The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL 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 and reverse proxy. Its event-driven, asynchronous architecture makes it highly efficient for handling concurrent connections. For a Laravel application, we’ll focus on optimizing Nginx for static file serving, SSL termination, and proxying requests to our application server (Gunicorn or PHP-FPM).
Core Nginx Configuration for Laravel
A typical Nginx configuration for a Laravel application involves a `server` block. We’ll prioritize efficient static file handling and proper proxying. The following configuration snippet assumes you are using Gunicorn as your application server, listening on a Unix socket for performance gains.
`nginx.conf` (or a site-specific conf file in `sites-available`)
# Global settings (consider tuning worker_processes based on CPU cores)
user www-data;
worker_processes auto; # Or set to the number of CPU cores
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024; # Adjust based on expected load and memory
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hide Nginx version for security
include /etc/nginx/mime.types;
default_type application/octet-stream;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m; # Adjust size as needed
ssl_session_timeout 10m;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Include virtual host configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
# Define upstream for Gunicorn (if using Unix socket)
upstream app_server {
server unix:/var/www/your_laravel_app/storage/app/sockets/gunicorn.sock fail_timeout=0;
}
# Define upstream for PHP-FPM (if using PHP-FPM)
# upstream php_fpm {
# server unix:/run/php/php8.1-fpm.sock; # Adjust PHP version as needed
# }
}
Laravel Site Configuration
This `server` block is crucial. It handles SSL, static file serving, and proxies dynamic requests to the upstream application server. We’ll configure caching for static assets and ensure proper headers are passed.
server {
listen 80;
server_name your_domain.com www.your_domain.com;
# Redirect HTTP to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name your_domain.com www.your_domain.com;
# SSL Certificate configuration
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; # Recommended by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # Recommended by Certbot
# Root directory for static files
root /var/www/your_laravel_app/public;
index index.php index.html index.htm;
# Static file caching
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off; # Optionally disable access logs for static files
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Laravel routing and proxying
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Proxy to Gunicorn
location ~ \.php$ {
# This block is for PHP-FPM. If using Gunicorn, this is not needed.
# If using PHP-FPM, uncomment and configure the upstream 'php_fpm' above.
# include snippets/fastcgi-php.conf;
# fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Adjust PHP version
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# include fastcgi_params;
}
# Proxy to Gunicorn (if using Gunicorn)
location ~* ^/(api|.*\.php)?$ { # Adjust regex if needed for specific routes or if not using .php
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://app_server;
proxy_read_timeout 300s; # Increase timeout for long-running requests
proxy_connect_timeout 75s;
}
# Deny access to sensitive files
location ~* /(composer\.json|composer\.lock|\.env|\.git|\.env\.example) {
deny all;
}
# Error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Tuning Nginx Worker Connections and Buffers
The worker_connections directive in the events block controls the maximum number of simultaneous connections that each worker process can open. The total maximum connections is worker_processes * worker_connections. For OVH instances, especially those with ample RAM, you can often increase this value. A common starting point is 1024, but for high-traffic sites, consider 4096 or even higher, provided your system has enough file descriptors and memory.
client_body_buffer_size and client_max_body_size are important for handling large uploads. If your application allows file uploads, ensure these are set appropriately. A common value for client_max_body_size might be 50M or 100M, depending on your requirements.
# In the http block: client_body_buffer_size 10M; # Default is 16k, increase for larger POST requests client_max_body_size 100M; # Maximum allowed size for uploaded files
SSL/TLS Optimization
Using HTTP/2 is essential for modern web performance. Ensure listen 443 ssl http2; is present. The SSL cipher suite selection is critical for security and performance. The provided list is a good starting point, prioritizing modern, secure ciphers. Regularly review and update these based on security advisories.
ssl_session_cache and ssl_session_timeout help reduce the overhead of SSL handshakes for returning clients. The size of the cache (e.g., 10m) should be tuned based on your server’s memory and expected concurrent SSL connections.
Gunicorn: The Python WSGI HTTP Server
Gunicorn is a popular WSGI HTTP Server for Python. It’s robust, performant, and well-suited for production environments. When deploying Laravel with a Python backend (e.g., using a framework like Flask or Django, or even a custom Python API that your Laravel app communicates with), Gunicorn is a strong choice. For this playbook, we’ll assume Gunicorn is serving a Python API that Laravel interacts with, or if you’re using a Python-based CMS/framework that Laravel is fronting.
Gunicorn Configuration for Performance
The primary Gunicorn configuration involves setting the number of worker processes and threads. The optimal configuration depends heavily on your server’s CPU cores and memory. A common starting point is to use the sync worker class, with a number of workers equal to (2 * number_of_cores) + 1. For I/O-bound applications, consider the gevent or event worker classes, which support asynchronous I/O and can handle more concurrent connections with fewer processes.
Starting Gunicorn (Systemd Service)
It’s best practice to manage Gunicorn via systemd. This ensures it starts on boot, restarts on failure, and is managed as a service.
# Create a virtual environment for your Python app python3 -m venv /var/www/your_python_api/venv source /var/www/your_python_api/venv/bin/activate pip install gunicorn your_python_api_package # Create a systemd service file sudo nano /etc/systemd/system/your_python_api.service
[Unit]
Description=Gunicorn instance to serve your_python_api
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/your_python_api
ExecStart=/var/www/your_python_api/venv/bin/gunicorn \
--workers 3 \
--bind unix:/var/www/your_laravel_app/storage/app/sockets/gunicorn.sock \
--timeout 120 \
--log-level info \
your_python_api.wsgi:application # Replace with your actual WSGI application entry point
# If using gevent worker class:
# ExecStart=/var/www/your_python_api/venv/bin/gunicorn \
# --workers 3 \
# --worker-class gevent \
# --bind unix:/var/www/your_laravel_app/storage/app/sockets/gunicorn.sock \
# --timeout 120 \
# --log-level info \
# your_python_api.wsgi:application
[Install]
Section=multi-user.target
After creating the service file, enable and start it:
sudo systemctl daemon-reload sudo systemctl enable your_python_api.service sudo systemctl start your_python_api.service sudo systemctl status your_python_api.service
Tuning Gunicorn Workers and Threads
The --workers flag is critical. For CPU-bound tasks, a good starting point is (2 * CPU_CORES) + 1. For I/O-bound tasks, especially with gevent or event workers, you can often use more workers, or rely on threads within workers if your worker class supports it (e.g., Gunicorn with Uvicorn workers). The --timeout value should be set high enough to accommodate your longest-running requests, but not so high that it masks application issues.
Binding to a Unix socket (unix:/path/to/socket.sock) is generally faster than binding to a TCP port for local communication between Nginx and Gunicorn, as it avoids the overhead of the TCP/IP stack.
PHP-FPM: The PHP Process Manager
If your Laravel application is running directly on PHP, PHP-FPM (FastCGI Process Manager) is the standard and highly performant way to interface PHP with web servers like Nginx. Proper tuning of PHP-FPM pools is essential for handling concurrent PHP requests efficiently.
PHP-FPM Configuration Tuning
PHP-FPM pools are configured in /etc/php/[version]/fpm/pool.d/www.conf (or a custom pool file). The key directives to tune are related to process management and resource limits.
; /etc/php/8.1/fpm/pool.d/www.conf (Example for PHP 8.1) ; Choose the process manager. 'dynamic' is recommended for most cases. ; 'static' can offer slightly better performance if memory usage is predictable. pm = dynamic ; For 'dynamic' PM: ; The number of child processes that will be spawned when pm.min_spare_servers ; are reached. pm.max_children = 100 ; The desired maximum number of child processes. ; This value should be calculated based on your server's RAM. ; A rough estimate: (Total RAM - RAM for OS/other services) / Average PHP process memory usage. ; Example: If you have 4GB RAM, and PHP processes use ~30MB each, you might aim for ~100-120. ; The minimum number of idle script arithmetical operators to use. pm.min_spare_servers = 10 ; The maximum number of idle script arithmetical operators to use. pm.max_spare_servers = 20 ; The number of requests each child process should execute before respawning. ; This helps to free up resources and prevent memory leaks. pm.max_requests = 500 ; For 'static' PM (use with caution, requires careful RAM calculation): ; pm.static_children = 50 ; pm.max_requests = 0 ; Disable respawning if static ; Listen on a Unix socket for better performance with Nginx listen = /run/php/php8.1-fpm.sock ; Set user and group user = www-data group = www-data ; Set permissions for the socket listen.owner = www-data listen.group = www-data listen.mode = 0660 ; Other useful settings: ; request_terminate_timeout = 120s ; Timeout for individual requests ; process_idle_timeout = 10s ; Timeout for idle processes
Tuning PHP-FPM Process Manager (PM)
The pm directive can be set to dynamic, static, or ondemand.
dynamic: PHP-FPM spawns children as needed, up topm.max_children, and kills idle ones to save memory. This is generally the best balance.static: PHP-FPM spawns a fixed number of children (pm.static_children) on startup and keeps them running. This can offer slightly better performance by eliminating the overhead of spawning/killing processes, but it consumes more memory constantly. Use this only if you have a very good understanding of your memory usage.ondemand: Processes are spawned only when a request is received and killed after a period of inactivity. This saves memory but can introduce latency on the first request after idle periods.
The calculation for pm.max_children (for dynamic) or pm.static_children (for static) is critical. A common formula is to estimate the total available RAM for PHP-FPM, subtract RAM for the OS and other services, and divide by the average memory footprint of a single PHP-FPM worker process. You can monitor PHP-FPM memory usage using tools like htop or by inspecting /proc/[pid]/smaps.
pm.max_requests is important for preventing memory leaks in long-running PHP applications. Respawning processes after a certain number of requests helps ensure a clean slate.
Nginx Configuration for PHP-FPM
Ensure your Nginx configuration correctly passes requests to PHP-FPM. If you are using the Unix socket, the fastcgi_pass directive should point to it. The snippets/fastcgi-php.conf file (often included by default) contains essential FastCGI parameters.
# In your Nginx server block, for PHP requests:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# If using Unix socket:
fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Adjust PHP version
# If using TCP socket (e.g., 127.0.0.1:9000):
# fastcgi_pass 127.0.0.1:9000;
}
PostgreSQL Tuning for Laravel Applications
PostgreSQL is a powerful and reliable relational database. Optimizing its performance is crucial for any data-intensive Laravel application. Tuning involves adjusting memory parameters, connection pooling, and query optimization.
Key PostgreSQL Configuration Parameters
The primary configuration file is postgresql.conf. The most impactful parameters for performance are related to memory allocation.
# postgresql.conf (Example settings for a 4GB RAM server)
# Shared Memory
shared_buffers = 1GB # Typically 25% of total RAM, but can go up to 40% on dedicated DB servers.
# For a 4GB server, 1GB is a good start.
# Memory for Sorting and Hashing
work_mem = 64MB # Memory used for internal sort operations and hash tables.
# Increase if you have complex queries with large sorts/hashes.
# Start with 16MB and increase based on query analysis.
# Set per-session, so don't over-allocate.
# Memory for Maintenance Operations
maintenance_work_mem = 256MB # Memory used for VACUUM, CREATE INDEX, etc.
# Can be set higher than work_mem.
# Connection Pooling
max_connections = 100 # Number of concurrent connections.
# Adjust based on your application's needs and server resources.
# Each connection consumes RAM.
# WAL (Write-Ahead Logging)
wal_buffers = 16MB # Memory for WAL data before writing to disk.
# Default is usually fine, but can be increased.
wal_writer_delay = 200ms # How often WAL writer wakes up.
# Checkpointing
checkpoint_timeout = 5min # Time between automatic WAL checkpoints.
max_wal_size = 4GB # Maximum size of WAL files before checkpointing.
# Larger values reduce checkpoint frequency, improving write performance
# but increasing recovery time after a crash.
# Autovacuum Tuning (Crucial for performance and preventing bloat)
autovacuum = on
autovacuum_max_workers = 3 # Number of autovacuum worker processes.
autovacuum_naptime = 1min # How often autovacuum checks for work.
autovacuum_vacuum_threshold = 50 # Minimum number of rows modified before vacuum is considered.
autovacuum_analyze_threshold = 50 # Minimum number of rows inserted/updated/deleted before analyze is considered.
autovacuum_vacuum_scale_factor = 0.1 # Percentage of table size to trigger vacuum.
autovacuum_analyze_scale_factor = 0.1 # Percentage of table size to trigger analyze.
Tuning PostgreSQL Memory Parameters
shared_buffers: This is the most critical parameter. It’s the amount of memory PostgreSQL uses for caching data pages. Setting it too low will lead to excessive disk I/O. Setting it too high can starve the OS and other processes. A common recommendation is 25% of system RAM for dedicated database servers, but on shared hosting or VPSs, you might need to be more conservative (e.g., 1GB on a 4GB VPS).
work_mem: This is the memory used for sorting, hashing, and other intermediate operations within a query. It’s allocated per operation, per query. If you have many concurrent complex queries, you might need to increase this, but be cautious as it can lead to high memory consumption if over-allocated. Monitor your database’s memory usage and query performance.
maintenance_work_mem: This parameter affects operations like VACUUM, CREATE INDEX, and ALTER TABLE. Increasing it can significantly speed up these maintenance tasks.
Connection Pooling with PgBouncer
Managing database connections can be a significant overhead. For applications with many short-lived connections (common in web applications), connection pooling is essential. PgBouncer is a lightweight connection pooler for PostgreSQL.
PgBouncer Configuration (`pgbouncer.ini`)
[databases] # Format: database_name = connection_string # Example: your_laravel_db = host=127.0.0.1 port=5432 dbname=your_laravel_db [pgbouncer] # Listen address and port for PgBouncer listen_addr = 127.0.0.1 listen_port = 6432 # Pool mode: # session: Each client connection is assigned to a server connection for its lifetime. # transaction: A server connection is assigned to a client connection only for the duration of a transaction. # statement: A server connection is assigned to a client connection only for the duration of a single statement. (Recommended for most Laravel apps) pool_mode = transaction # Maximum number of server connections to pool for each database. max_client_conn = 200 # Maximum number of server connections to pool for each database. max_db_connections = 50 # Authentication method. 'md5' is common. 'trust' for local connections if secure. auth_type = md5 auth_user = pgbouncer_user # A dedicated user for PgBouncer # User list file auth_file = /etc/pgbouncer/userlist.txt # Logging logfile = /var/log/pgbouncer/pgbouncer.log pidfile = /var/run/pgbouncer/pgbouncer.pid # Other settings server_reset_query = DISCARD ALL; server_check_query = SELECT 1;
Create the userlist.txt file with credentials for PgBouncer to connect to PostgreSQL:
# /etc/pgbouncer/userlist.txt # Format: "database" "user" "password" "your_laravel_db" "pgbouncer_user" "your_secure_password"
You’ll need to create the pgbouncer_user in your PostgreSQL database and grant it necessary permissions. Then, configure your Laravel application to connect to PgBouncer instead of directly to PostgreSQL. Update your .env file:
DB_HOST=127.0.0.1 DB_PORT=6432 # PgBouncer's listen_port DB_DATABASE=your_laravel_db DB_USERNAME=pgbouncer_user # Or the user defined in userlist.txt DB_PASSWORD=your_secure_password
Autovacuum Tuning
Autovacuum is essential for reclaiming space from dead tuples and preventing table bloat, which can severely degrade performance. The default settings are often too conservative for busy databases. Tuning autovacuum_vacuum_threshold, autovacuum_analyze_threshold, and their _scale_factor counterparts is key. For very large tables, using scale factors (e.g., 0.1 for 10%) is generally better than fixed thresholds.
autovacuum_max_workers should be set to a reasonable number based on your CPU cores to allow concurrent vacuuming of multiple tables.
OVH Specific Considerations
OVH offers a range of VPS and dedicated server options. The key is to understand your resource allocation (CPU, RAM, Disk I/O) and tailor these configurations accordingly.
- CPU Cores: Use this to determine
worker_processesin Nginx and the number of Gunicorn workers. - RAM: This is the most critical resource for PostgreSQL (
shared_buffers,work_mem) and PHP-FPM (pm.max_children). - Disk I/O: For databases, especially PostgreSQL, fast disk I/O is paramount. OVH’s SSD or NVMe options will significantly outperform traditional HDDs. Ensure your PostgreSQL data directory is on the fastest available storage.
- Network Bandwidth: While less of a tuning parameter, ensure your OVH plan provides sufficient bandwidth for your expected traffic.
When using OVH’s managed PostgreSQL services, you may have less direct control over postgresql.conf, but you can often influence parameters through their control panel or by contacting support. For self-managed PostgreSQL on a VPS or dedicated server, direct tuning is possible.
Monitoring and Iteration
Tuning is not a one-time event. Continuous monitoring is essential.
- Nginx: Monitor access logs for errors (5xx codes), slow requests, and traffic patterns. Use tools like
ngx_http_stub_status_modulefor real-time metrics. - Gunicorn/PHP-FPM: Monitor logs for errors and exceptions. Use system monitoring tools (
htop,top,vmstat) to track CPU and memory usage. - PostgreSQL: Use
pg_stat_activityto see active queries,pg_stat_statements(requires extension) for query performance analysis, and tools likepgBadgerfor log analysis. Monitor disk I/O usingiostat. - Application Performance Monitoring (APM): Tools like New Relic, Datadog, or Sentry can provide deep insights into application bottlenecks, including database query times and external API calls.
Start with conservative settings, monitor performance, and iteratively adjust parameters. Make one change at a time and measure its impact. This systematic approach will lead to a highly optimized and stable Laravel deployment on OVH.