The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on OVH for WordPress
Nginx as a High-Performance Frontend for WordPress
When deploying WordPress on a modern infrastructure, Nginx serves as an exceptionally efficient web server and reverse proxy. Its event-driven, asynchronous architecture excels at handling a high volume of concurrent connections with minimal resource overhead. For WordPress, this translates to faster response times and better scalability. We’ll focus on tuning Nginx for static file serving, SSL termination, and efficient proxying to your PHP-FPM or Gunicorn backend.
Core Nginx Configuration Tuning
The primary Nginx configuration file, typically located at /etc/nginx/nginx.conf, contains global settings that influence worker processes and connection handling. For a production environment, especially on OVH’s robust infrastructure, we can push these limits higher than a typical shared hosting setup.
Start by adjusting the worker_processes directive. Setting this to the number of CPU cores available on your server is a common and effective practice. You can determine the number of cores using nproc.
Determining Worker Processes
nproc
Then, update your nginx.conf:
user www-data;
worker_processes auto; # Or set to the number of CPU cores, e.g., worker_processes 4;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Increase based on server capacity and expected load
multi_accept on;
}
The worker_connections directive defines the maximum number of simultaneous connections that each worker process can open. A value of 4096 is a good starting point, but this can be increased further if your server has ample RAM and you anticipate very high concurrency. The multi_accept on; directive allows a worker to accept as many new connections as possible per event loop iteration.
Optimizing WordPress Site Configuration
Within your WordPress site’s specific Nginx configuration file (e.g., /etc/nginx/sites-available/your-wordpress-site.conf), several directives are crucial for performance. Caching, Gzip compression, and efficient file serving are paramount.
Static File Caching and Compression
server {
listen 80;
listen [::]:80;
server_name your-domain.com www.your-domain.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com www.your-domain.com;
# SSL Configuration (ensure you have valid certificates)
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;
# 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;
# Static File Caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2|ttf|eot)$ {
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
log_not_found off;
}
# WordPress specific rules
root /var/www/your-wordpress-site/public_html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP-FPM or Gunicorn Proxy Configuration
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# For PHP-FPM
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and socket path
# For Gunicorn (if using a WSGI app like Flask/Django for WordPress API or headless)
# fastcgi_pass unix:/path/to/your/gunicorn.sock;
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Other security headers and configurations
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self'; media-src 'self'; frame-src 'self'; img-src 'self' data:;"; # Example CSP, adjust as needed
}
Key directives here:
gzip on;and related directives: Enable and configure Gzip compression for text-based assets.location ~* \.(jpg|...)$: This block targets static assets.expires 365d;sets a long cache expiry for browsers, andadd_header Cache-Control "public, immutable";instructs browsers and intermediate caches to aggressively cache these files.access_log off;andlog_not_found off;reduce I/O for static files.rootandindex: Define the document root and default index files for your WordPress installation.location / { try_files $uri $uri/ /index.php?$args; }: This is the standard WordPress Nginx rewrite rule, ensuring that requests for non-existent files or directories are passed toindex.phpfor WordPress to handle.location ~ \.php$: This block configures how Nginx communicates with your PHP processor (PHP-FPM in this case). Ensure thefastcgi_passdirective points to the correct PHP-FPM socket.- Security headers: Essential for protecting your site against various web vulnerabilities.
Nginx as a Reverse Proxy for Gunicorn/PHP-FPM
When Nginx acts as a reverse proxy, it forwards dynamic requests to a backend application server like Gunicorn (for Python WSGI apps) or PHP-FPM. This offloads the heavy lifting of processing dynamic content from Nginx, allowing Nginx to focus on its strengths: serving static files and managing connections.
Tuning PHP-FPM
PHP-FPM (FastCGI Process Manager) is the de facto standard for running PHP. Its configuration, typically found in /etc/php/8.1/fpm/php-fpm.conf and pool configurations in /etc/php/8.1/fpm/pool.d/www.conf (or a custom pool name), is critical. For high-traffic WordPress sites, tuning the process manager is essential.
Process Manager Settings
; /etc/php/8.1/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock # Or a TCP/IP socket like 127.0.0.1:9000 listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic # Options: static, dynamic, ondemand pm.max_children = 100 # Adjust based on RAM and CPU pm.start_servers = 5 pm.min_spare_servers = 2 pm.max_spare_servers = 10 pm.process_idle_timeout = 10s pm.max_requests = 500 # Restart worker after this many requests to prevent memory leaks
Explanation of key directives:
pm:dynamicis generally recommended. It allows PHP-FPM to scale the number of worker processes based on load.statickeeps a fixed number of workers, which can be good for predictable loads but might waste resources.ondemandstarts workers only when needed, saving resources but introducing latency on the first request.pm.max_children: This is the most critical setting. It defines the maximum number of PHP-FPM worker processes that can run simultaneously. Setting this too high can exhaust server RAM, leading to OOM killer events. Set it based on your server’s RAM and the typical memory footprint of your WordPress PHP processes. A common starting point isRAM_AVAILABLE / AVERAGE_PHP_PROCESS_MEMORY.pm.start_servers,pm.min_spare_servers,pm.max_spare_servers: These control how PHP-FPM manages its pool of workers when using thedynamicorondemandprocess managers.pm.max_requests: Setting this to a reasonable number (e.g., 500-1000) helps prevent memory leaks in long-running PHP scripts or plugins from consuming all available memory over time.
Tuning Gunicorn (for Headless WordPress/API)
If you’re using WordPress in a headless configuration with a Python backend (e.g., Django or Flask) serving the API, Gunicorn is a popular WSGI HTTP Server. Tuning Gunicorn involves managing worker processes and threads.
Gunicorn Worker Configuration
# Example command to start Gunicorn # Adjust workers and threads based on your server's CPU cores and RAM. # A common starting point is (2 * CPU_CORES) + 1 workers. # Threads are useful for I/O-bound tasks. gunicorn --workers 4 --threads 2 --bind unix:/path/to/your/gunicorn.sock your_wsgi_app:app --timeout 120 --graceful-timeout 120 --log-level info --access-logfile /var/log/gunicorn/access.log --error-logfile /var/log/gunicorn/error.log
Key Gunicorn parameters:
--workers: The number of worker processes. A good rule of thumb is(2 * number_of_cpu_cores) + 1.--threads: The number of threads per worker. This is beneficial for I/O-bound operations, allowing a worker to handle multiple requests concurrently if they are waiting on external resources.--bind: The address to bind to. Using a Unix socket (unix:/path/to/socket) is generally more performant than a TCP socket (127.0.0.1:8000) for local communication between Nginx and Gunicorn.--timeout: The maximum time in seconds that a worker can spend on a request before being killed.--graceful-timeout: The maximum time in seconds that Gunicorn will wait for existing requests to finish during a graceful restart.
PostgreSQL Performance Tuning for WordPress
For WordPress sites that rely on PostgreSQL (often via plugins like “WP Migrate DB Pro” or for custom applications), tuning the database is as crucial as tuning the web server. OVH’s VPS and Dedicated servers offer robust PostgreSQL instances, but proper configuration is key.
Key PostgreSQL Configuration Parameters
The primary PostgreSQL configuration file is postgresql.conf. Its location varies by version and OS, but it’s often found in /etc/postgresql/[version]/main/postgresql.conf.
Memory and Caching
# postgresql.conf
# Shared Memory
shared_buffers = 1GB # Adjust based on server RAM. Typically 25% of total RAM.
# For 8GB RAM, 2GB is a good starting point.
# For 16GB RAM, 4GB. For 32GB+ RAM, 8GB.
# WAL (Write-Ahead Logging)
wal_buffers = 16MB # Default is -1 (auto-tuned based on shared_buffers), but explicit is fine.
wal_writer_delay = 200ms # Default is 200ms.
wal_checkpoint_timeout = 30min # Default is 5min. Increase to reduce I/O spikes.
wal_checkpoint_completion_target = 0.9 # Default is 0.5. Smooths out checkpoint I/O.
# Background Writer
bgwriter_delay = 10ms # Default is 10ms.
bgwriter_lru_maxpages = 1000 # Default is 100. Increase to allow background writer to clean more pages.
bgwriter_lru_multiplier = 1.0 # Default is 1.0.
# Effective Cache Size
# This tells PostgreSQL how much memory is available for OS and other caches.
# It influences the planner's decision on index usage.
# Set to roughly 50-75% of total system RAM.
# For 16GB RAM, this could be 12GB (12288MB).
effective_cache_size = 12GB
# Maintenance Work Memory
# Used for VACUUM, CREATE INDEX, ALTER TABLE ADD FOREIGN KEY.
# Larger values can speed up these operations significantly.
# Set to 5-10% of total RAM, but not more than 2GB for typical WP workloads.
maintenance_work_mem = 512MB
# Work Memory
# Used for sorting and hashing in queries.
# Default is 4MB. Can be increased if you have complex queries or large sorts.
# Be cautious, as this is per-operation, per-connection.
# For WordPress, often not excessively high.
work_mem = 16MB
# Autovacuum
autovacuum = on
autovacuum_max_workers = 3 # Default is 3. Increase if you have many tables and high churn.
autovacuum_naptime = 1min # Default is 1min.
autovacuum_vacuum_threshold = 50 # Default is 50.
autovacuum_analyze_threshold = 50 # Default is 50.
Tuning these parameters requires understanding your server’s RAM and the nature of your WordPress workload:
shared_buffers: This is the most important parameter for caching. It’s the amount of memory PostgreSQL uses to cache data. Aim for 25% of your server’s RAM, but don’t exceed 8GB on systems with less than 32GB RAM, as the OS also needs memory.wal_buffers,wal_checkpoint_timeout,wal_checkpoint_completion_target: These tune Write-Ahead Logging. Increasingwal_checkpoint_timeoutandwal_checkpoint_completion_targetcan reduce the frequency and impact of I/O spikes caused by checkpoints, which is beneficial for performance.effective_cache_size: Crucial for the query planner. Setting this accurately helps PostgreSQL decide whether to use indexes or sequential scans. It should reflect the total memory available for caching, including OS caches.maintenance_work_mem: Essential for operations likeVACUUM(which WordPress plugins often trigger) and index creation. Increasing this can significantly speed up these maintenance tasks.work_mem: Affects sorting and hashing. For typical WordPress queries, high values are often not needed, but if you encounter slow queries involving sorts, this can be a factor.autovacuum: Ensure autovacuum is enabled. It’s vital for reclaiming space from dead rows and preventing table bloat. Tuning its parameters can ensure it runs efficiently without impacting foreground operations.
Connection Pooling
WordPress, especially with many plugins, can open a large number of database connections. Each connection consumes memory. Using a connection pooler like PgBouncer can significantly reduce overhead and improve performance by reusing existing connections.
PgBouncer Configuration Example
# /etc/pgbouncer/pgbouncer.ini [databases] # Format: dbname = host=host port=port user=user password=password # For local connections, use 'host=/path/to/socket' wordpress_db = host=/var/run/postgresql port=5432 user=wordpress_user dbname=wordpress_db password=your_db_password [pgbouncer] listen_addr = 127.0.0.1 # Or the IP address your application connects to listen_port = 6432 auth_type = md5 # Or scram-sha-256 for better security auth_file = /etc/pgbouncer/userlist.txt pool_mode = session # 'transaction' mode can be problematic for some WP plugins default_pool_size = 20 # Number of connections per database in the pool max_client_conn = 1000 # Maximum number of clients that can connect to PgBouncer default_max_client_conn = 100 # Max clients per database min_pool_size = 5 pool_timeout = 300 # Seconds to keep idle connections open # Logging logfile = /var/log/postgresql/pgbouncer.log pidfile = /var/run/pgbouncer/pgbouncer.pid log_connections = 0 log_disconnections = 0 log_pooler_errors = 1
And the corresponding userlist.txt:
# /etc/pgbouncer/userlist.txt # Format: "dbname" "user" "password" "wordpress_db" "wordpress_user" "your_db_password"
After configuring PgBouncer, you’ll need to adjust your WordPress database connection details (usually in wp-config.php) to point to PgBouncer’s port (e.g., 6432) instead of the direct PostgreSQL port (5432).
Monitoring and Iterative Tuning
Infrastructure tuning is not a one-time event. Continuous monitoring and iterative adjustments are crucial for maintaining optimal performance. On OVH, leverage their monitoring tools and supplement them with standard Linux utilities and application-specific metrics.
Essential Monitoring Tools
- System Metrics: Use
top,htop,vmstat,iostat, andsarto monitor CPU, memory, disk I/O, and network usage. Pay close attention to swap usage (should be minimal) and I/O wait times. - Nginx Metrics: Enable Nginx’s
stub_statusmodule to get basic connection statistics. For more advanced metrics, consider Prometheus with thenginx-exporter. - PHP-FPM Metrics: PHP-FPM also has a status page that can be enabled.
- PostgreSQL Metrics: Use
pg_stat_activity,pg_stat_statements(requires installation and configuration), and tools likepg_topor Prometheus withpostgres_exporter. - Application Performance Monitoring (APM): Tools like New Relic, Datadog, or open-source alternatives like Jaeger/Prometheus can provide deep insights into WordPress plugin performance, slow database queries, and external API calls.
Iterative Tuning Workflow
- Establish a Baseline: Before making any changes, record current performance metrics under typical load.
- Identify Bottlenecks: Use monitoring tools to pinpoint the slowest component (CPU, RAM, Disk I/O, Network, Database, Application Code).
- Make One Change at a Time: Adjust a single configuration parameter or setting.
- Test and Measure: Re-run load tests or observe live traffic to see the impact of the change. Compare against the baseline.
- Document: Record the change, its impact, and the new baseline.
- Repeat: Continue the cycle until performance targets are met or further improvements yield diminishing returns.
For example, if you observe high CPU usage on your web server, you might first investigate Nginx and PHP-FPM configurations. If database queries are slow, focus on PostgreSQL tuning and connection pooling. If memory is the constraint, review max_children for PHP-FPM or Gunicorn worker/thread counts.