The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Elasticsearch on OVH for WordPress
Nginx Configuration for WordPress High-Traffic Scenarios
Optimizing Nginx for a high-traffic WordPress site involves fine-tuning worker processes, connection limits, caching, and static file serving. On OVH infrastructure, leveraging their robust network and dedicated resources is key. We’ll focus on `nginx.conf` and site-specific configurations.
Tuning Worker Processes and Connections
The `worker_processes` directive should ideally be set to the number of CPU cores available. For dynamic tuning based on load, `auto` can be used, but a fixed number often provides more predictable performance. `worker_connections` dictates the maximum number of simultaneous connections a worker can handle. This needs to be balanced with the system’s `ulimit` settings.
System-Level Limits
Before adjusting Nginx, ensure the operating system’s file descriptor limits are sufficient. Edit `/etc/security/limits.conf` and add or modify these lines (replace `www-data` with your Nginx user if different):
* soft nofile 65536 * hard nofile 65536 www-data soft nofile 65536 www-data hard nofile 65536
Then, edit `/etc/sysctl.conf` to increase the maximum number of open files system-wide:
fs.file-max = 2097152
Apply these changes with `sysctl -p` and verify with `ulimit -n` (for the current shell) and `cat /proc/sys/fs/file-max`.
Nginx `nginx.conf` Tuning
In your main `nginx.conf` (typically `/etc/nginx/nginx.conf`), adjust the `events` block:
user www-data www-data;
worker_processes auto; # Or set to number of CPU cores
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on expected load and ulimit
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ... other http configurations ...
}
Gunicorn Configuration for WordPress (with WSGI)
While WordPress is traditionally PHP-based, modern architectures might use Gunicorn to serve a Python WSGI application that interacts with WordPress (e.g., via its REST API or a headless CMS setup). If you’re running a PHP application with PHP-FPM, skip this section. For a Python WSGI app:
Gunicorn Worker Types and Scaling
Gunicorn’s performance is heavily influenced by its worker type and the number of workers. For I/O-bound applications (common with web apps), the `gevent` or `event` workers are preferred. The number of workers is typically set to `(2 * number_of_cores) + 1` as a starting point.
Gunicorn Command-Line Options
A typical Gunicorn startup command for a WSGI application (`your_app.wsgi:application`) might look like this:
gunicorn --workers 4 --worker-class gevent --bind 0.0.0.0:8000 your_app.wsgi:application
For production, it’s recommended to run Gunicorn under a process manager like `systemd` or `supervisor`. Here’s a sample `systemd` service file (`/etc/systemd/system/gunicorn.service`):
[Unit] Description=Gunicorn instance to serve your_app After=network.target [Service] User=your_user Group=your_group WorkingDirectory=/path/to/your/app ExecStart=/path/to/your/venv/bin/gunicorn --workers 4 --worker-class gevent --bind unix:/run/gunicorn.sock your_app.wsgi:application # Or for TCP binding: # ExecStart=/path/to/your/venv/bin/gunicorn --workers 4 --worker-class gevent --bind 0.0.0.0:8000 your_app.wsgi:application Restart=always RestartSec=5s [Install] WantedBy=multi-user.target
After creating the service file, enable and start it:
sudo systemctl enable gunicorn sudo systemctl start gunicorn sudo systemctl status gunicorn
PHP-FPM Configuration for WordPress
For standard WordPress deployments, PHP-FPM is the engine. Tuning its process manager is critical. The most common process managers are `static`, `dynamic`, and `ondemand`. For high-traffic sites, `dynamic` or `static` are generally preferred over `ondemand`.
Choosing a Process Manager
- static: Pre-forks a fixed number of child processes. Best for predictable, high-load environments where memory is not a constraint.
- dynamic: Starts with a few processes and spawns more as needed, up to a `pm.max_children` limit. Processes are killed if idle. Good balance.
- ondemand: Spawns processes only when requests arrive. Can save memory but introduces latency for the first request after an idle period. Not ideal for high-traffic, low-latency needs.
PHP-FPM Pool Configuration (`www.conf`)
Locate your PHP-FPM pool configuration file, typically `/etc/php/X.Y/fpm/pool.d/www.conf` (replace X.Y with your PHP version). Here’s a tuned example using the `dynamic` process manager:
; Start a new pool named 'www'. [www] ; Unix user/group of processes user = www-data group = www-data ; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'listen = /run/php/phpX.Y-fpm.sock' (for Unix sockets, recommended for performance) ; 'listen = 127.0.0.1:9000' (for TCP/IP sockets) listen = /run/php/php8.1-fpm.sock ; Adjust PHP version ; Listen queue. Maximum number of the children that can be waiting for processing. ; Triggered when pm.max_children is reached. Default Value: 0 listen.backlog = 511 ; Number of child processes to kill after 30 seconds of inactivity. pm.max_requests = 500 ; Helps prevent memory leaks ; Choose how the process manager (pm) should behave. ; Available values: 'dynamic', 'static', 'ondemand'. pm = dynamic ; The method to use to start children. ; 'static' means always have this many children. ; 'dynamic' means start with pm.start_servers and grow to pm.max_children. ; 'ondemand' means start no children until first request. pm.max_children = 150 ; Adjust based on available RAM and CPU cores pm.min_spare_servers = 10 ; Minimum number of idle servers pm.max_spare_servers = 30 ; Maximum number of idle servers pm.start_servers = 5 ; Number of servers to start on boot ; The maximum number of processes that will be spawned. ; Default Value: not set ; pm.max_children = 50 ; If using 'static' pm, this is the fixed number ; If using 'dynamic' pm, this is the number of child processes created on startup. ; Default Value: 0 ; pm.start_servers = 2 ; If using 'dynamic' pm, this is the desired number of top-level servers. ; Default Value: 5 ; pm.min_spare_servers = 1 ; pm.max_spare_servers = 3 ; If using 'dynamic' pm, the maximum number of seconds a child process may live without ; handling requests. The child process will be killed after this time. The default ; value is 0 (never stop). ; pm.max_requests = 0 ; Process Idle Timeout (seconds) ; pm.process_idle_timeout = 10s ; Default is 10s ; Request termination timeout (seconds) ; request_terminate_timeout = 0 ; Default is 0 (no timeout) ; Slowlog ; slowlog = /var/log/php/php8.1-fpm.slow.log ; request_slowlog_timeout = 10s ; Other useful settings ; php_admin_value[memory_limit] = 256M ; php_admin_value[upload_max_filesize] = 64M ; php_admin_value[post_max_size] = 64M ; php_admin_value[max_execution_time] = 300
After modifying `www.conf`, restart PHP-FPM:
sudo systemctl restart php8.1-fpm ; Adjust PHP version
Elasticsearch Tuning for WordPress Search
Integrating Elasticsearch with WordPress, often via plugins like “ElasticPress,” significantly enhances search capabilities. Tuning Elasticsearch itself is crucial for performance, especially under load.
JVM Heap Size Configuration
The most critical Elasticsearch tuning parameter is the JVM heap size. It should be set to no more than 50% of your system’s physical RAM, and never exceed 30-32GB due to compressed ordinary object pointers (compressed oops).
Edit the Elasticsearch JVM options file. The location varies by installation method:
- Debian/Ubuntu (package): `/etc/elasticsearch/jvm.options`
- Tarball: `config/jvm.options` within the Elasticsearch installation directory
# Set Xms and Xmx to the same value for predictable performance. # Example for a server with 32GB RAM: -Xms16g -Xmx16g
Restart Elasticsearch after changes:
sudo systemctl restart elasticsearch
Index Settings and Sharding
For WordPress content, a single index is common. However, consider the number of shards and replicas. Too many shards can strain the cluster, while too few might limit parallelism. For a typical WordPress site, 1-3 primary shards per index is often sufficient. Replicas provide redundancy and can improve read performance but increase storage and indexing overhead.
You can set these during index creation or update them later. Example using `curl` to update settings for an index named `wordpress`:
# Check current settings
curl -X GET "localhost:9200/wordpress/_settings?pretty"
# Update settings (e.g., to 2 shards, 1 replica)
curl -X PUT "localhost:9200/wordpress/_settings?pretty" -H 'Content-Type: application/json' -d'
{
"index" : {
"number_of_shards" : "2",
"number_of_replicas" : "1"
}
}
'
Query Optimization and Caching
Elasticsearch has its own query cache and request cache. Ensure they are enabled and appropriately sized. For WordPress, focus on efficient search queries. Plugins like ElasticPress often handle much of this, but understanding the underlying Elasticsearch queries is beneficial.
Monitor Elasticsearch performance using tools like Kibana’s Stack Monitoring or dedicated APM solutions. Key metrics include JVM heap usage, CPU utilization, indexing latency, and search latency.
Putting It All Together: Nginx as a Reverse Proxy
Nginx will act as the primary entry point, routing traffic to either PHP-FPM (for dynamic WordPress content) or serving static assets directly. If using Gunicorn, Nginx would also proxy requests to Gunicorn.
Nginx Site Configuration Example
Consider a WordPress site served by PHP-FPM and static assets. Your Nginx virtual host configuration (e.g., `/etc/nginx/sites-available/your-wordpress-site`) might look like this:
server {
listen 80;
server_name your-domain.com www.your-domain.com;
root /var/www/your-wordpress-site;
index index.php index.html index.htm;
# SSL configuration (highly recommended)
# listen 443 ssl http2;
# 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;
# Cache static assets for a long time
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2)$ {
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
log_not_found off;
}
# Deny access to sensitive files
location ~ /\. {
deny all;
}
# Serve static files directly if they exist
location ~ ^/(wp-content/uploads/.*)$ {
try_files $uri =404;
}
# Pass PHP scripts to PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Adjust PHP version
# With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
# Include security headers and optimizations
fastcgi_read_timeout 300; # Increase timeout for long-running scripts
fastcgi_buffer_size 128k;
fastcgi_buffers 8 128k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
# WordPress specific rules
# try_files $uri =404; # This should be handled by WordPress itself
}
# WordPress permalink rules
location / {
try_files $uri $uri/ /index.php?$args;
}
# Security headers
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'; base-uri 'self'; form-action 'self'; frame-ancestors 'self';"; # Example CSP, requires careful tuning
# 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;
}
Enable the site and test Nginx configuration:
sudo ln -s /etc/nginx/sites-available/your-wordpress-site /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
This comprehensive approach, combining Nginx, PHP-FPM (or Gunicorn), and Elasticsearch tuning, provides a robust foundation for high-performance WordPress deployments on OVH infrastructure.