The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on OVH for WordPress
Nginx Configuration for High-Traffic WordPress on OVH
Optimizing Nginx is paramount for serving high-traffic WordPress sites. On OVH infrastructure, leveraging its robust network and dedicated resources requires a fine-tuned Nginx configuration. We’ll focus on caching, connection management, and static file serving.
Nginx Caching Strategies
Browser caching and server-side caching are distinct but complementary. For browser caching, we leverage `Expires` and `Cache-Control` headers. For server-side, Nginx’s FastCGI cache is highly effective for dynamic content generated by PHP.
Browser Caching Headers
This configuration snippet should be placed within your WordPress `server` block, typically targeting static assets.
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)$ {
expires 365d;
add_header Cache-Control "public, no-transform";
access_log off;
log_not_found off;
}
Here, we set a long expiration for static assets (365 days) and instruct browsers to cache them publicly. `no-transform` prevents intermediaries from modifying the content. Disabling access logs for these assets reduces I/O overhead.
Nginx FastCGI Caching
FastCGI caching stores the output of PHP scripts, significantly reducing the load on your PHP-FPM workers and database. First, define the cache zone in the `http` block:
http {
# ... other http directives ...
fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=wp_cache:100m inactive=60m max_size=10g;
fastcgi_temp_path /var/tmp/nginx/fastcgi_temp;
# ... other http directives ...
}
Explanation:
fastcgi_cache_path: Defines the directory for cache files, cache levels, and zone parameters./var/cache/nginx/wordpress: The directory where cache files will be stored. Ensure this directory exists and Nginx has write permissions.levels=1:2: Sets up a two-level directory structure for cache files to prevent too many files in a single directory.keys_zone=wp_cache:100m: Creates a shared memory zone namedwp_cachewith 100MB capacity to store cache keys. Adjust size based on expected cache hits.inactive=60m: Items not accessed for 60 minutes will be removed.max_size=10g: The maximum size of the cache on disk.fastcgi_temp_path: A temporary directory for FastCGI operations.
Next, configure your WordPress `location` block to utilize this cache. This typically involves setting cache keys, enabling the cache, and defining cache bypass conditions.
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust to your PHP-FPM version and socket path
# FastCGI Cache Configuration
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 302 10m; # Cache successful responses for 10 minutes
fastcgi_cache_valid 404 1m; # Cache 404s for 1 minute
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
fastcgi_cache_lock on;
fastcgi_cache_lock_timeout 5s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Add cache status header for debugging
add_header X-Cache-Status $upstream_cache_status;
# Bypass cache for logged-in users or specific URIs
set $skip_cache 0;
if ($http_cookie ~* "wordpress_logged_in|comment_author") {
set $skip_cache 1;
}
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/feed/") {
set $skip_cache 1;
}
if ($request_method = POST) {
set $skip_cache 1;
}
}
Key directives:
fastcgi_cache_key: Defines how cache entries are identified. Including scheme, method, host, and URI is standard.fastcgi_cache_valid: Specifies how long to cache different HTTP status codes.fastcgi_cache_use_stale: Allows Nginx to serve stale cache content if the backend is unavailable or slow.fastcgi_cache_lock: Prevents multiple requests for the same uncached resource from hitting the backend simultaneously.fastcgi_cache_bypassandfastcgi_no_cache: Use variables to control when the cache is bypassed.$skip_cache: A variable set to 1 to bypass the cache. We set it to 1 for logged-in users (detected via cookies), admin areas, login pages, XML-RPC, and POST requests.X-Cache-Status: A custom header to see if content is served from cache (HIT, MISS, BYPASS, EXPIRED, etc.). Essential for debugging.
Nginx Connection Tuning
Optimizing worker processes and connection limits is crucial for handling concurrent users. These directives are placed in the `http` block.
http {
# ... other http directives ...
worker_processes auto; # Or set to the number of CPU cores
worker_connections 4096; # Adjust based on server memory and expected load
multi_accept on;
keepalive_timeout 65;
keepalive_requests 1000;
# ... other http directives ...
}
Tuning:
worker_processes auto: Nginx will automatically determine the number of worker processes based on the number of CPU cores. This is generally a good starting point.worker_connections: The maximum number of simultaneous connections that each worker process can handle. The total maximum connections isworker_processes * worker_connections. This value should be set considering available RAM and expected load. A common starting point is 4096.multi_accept on: Allows a worker process to accept multiple new connections at once.keepalive_timeout: The time a persistent connection will remain open.keepalive_requests: The maximum number of requests that can be made over a single persistent connection.
Gunicorn Configuration for WordPress (via WSGI)
While WordPress is traditionally PHP-based, using Gunicorn with a WSGI application (like wordpress-wsgi or a custom handler) is an advanced setup for specific use cases, often involving Python-based plugins or custom logic. For standard WordPress, PHP-FPM is the norm. However, if you are in a mixed environment or have specific Python integration needs, here’s a Gunicorn tuning guide.
Gunicorn Worker Processes and Threads
The number of worker processes and threads significantly impacts concurrency. A common strategy is to use a mix of worker types.
# Example command line for starting Gunicorn gunicorn --workers 3 --threads 2 --bind 0.0.0.0:8000 wsgi:app
Explanation:
--workers: The number of worker processes. A common recommendation is(2 * number_of_cpu_cores) + 1.--threads: The number of threads per worker process. This is applicable forgthreadoruvicornworkers.--bind: The address and port Gunicorn listens on.wsgi:app: Assumes your WSGI application is namedappand is in a file namedwsgi.py.
For CPU-bound tasks, more workers are beneficial. For I/O-bound tasks, threads can improve concurrency within a worker. For WordPress, which is largely I/O bound (database, file system), a higher thread count might be considered, but always test thoroughly.
Gunicorn Timeouts and Keepalive
Setting appropriate timeouts prevents hung requests from blocking workers.
# Example command line for starting Gunicorn gunicorn --workers 3 --threads 2 --bind 0.0.0.0:8000 --timeout 30 --keep-alive 5 wsgi:app
Tuning:
--timeout: The number of seconds to wait for the application to respond. If the application takes longer, Gunicorn will kill the worker and restart it. Default is 30 seconds. Adjust based on expected request processing times.--keep-alive: The number of seconds to wait for a request on a keep-alive connection.
PHP-FPM Tuning for WordPress
For standard WordPress deployments on OVH, PHP-FPM is the engine. Tuning its process manager and memory limits is critical.
PHP-FPM Process Manager Settings
These settings are typically found in your PHP-FPM pool configuration file (e.g., /etc/php/8.1/fpm/pool.d/www.conf).
; For a server with 4 CPU cores and 16GB RAM, a good starting point: pm = dynamic pm.max_children = 100 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.process_idle_timeout = 10s pm.max_requests = 500
Explanation:
pm = dynamic: The process manager.dynamicis often preferred as it scales workers based on load.statickeeps a fixed number of workers running.pm.max_children: The maximum number of child processes that can be spawned. This is the most critical setting for memory usage. Set this based on your server’s available RAM. Calculate:(Total RAM - RAM for OS/other services) / Average RAM per PHP-FPM process.pm.start_servers: The number of child processes started when the pool starts.pm.min_spare_servers: The minimum number of idle processes that should be kept waiting.pm.max_spare_servers: The maximum number of idle processes that can be kept waiting.pm.process_idle_timeout: The number of seconds after which an idle process will be killed.pm.max_requests: The number of requests each child process will execute before respawning. This helps prevent memory leaks.
PHP Memory Limits
WordPress and its plugins can be memory-intensive. Adjusting memory_limit in php.ini is essential.
; In /etc/php/8.1/fpm/php.ini memory_limit = 256M upload_max_filesize = 64M post_max_size = 64M max_execution_time = 120
Tuning:
memory_limit: The maximum amount of memory a script can consume. 256MB is a good starting point for WordPress.upload_max_filesizeandpost_max_size: Important for handling media uploads. Ensurepost_max_sizeis greater than or equal toupload_max_filesize.max_execution_time: The maximum time a script is allowed to run before it’s terminated. Essential for long-running tasks like imports or complex queries.
Remember to restart PHP-FPM after making changes: sudo systemctl restart php8.1-fpm.
MySQL Tuning for WordPress on OVH
Database performance is often the bottleneck. Tuning MySQL’s configuration file (my.cnf or files in /etc/mysql/mysql.conf.d/) is crucial.
InnoDB Buffer Pool
The InnoDB buffer pool is where InnoDB caches table and index data. This is the single most important setting for InnoDB performance.
[mysqld] innodb_buffer_pool_size = 4G ; Adjust based on available RAM (e.g., 50-75% of dedicated RAM) innodb_buffer_pool_instances = 4 ; Number of buffer pool instances (typically 1 per GB of buffer pool size)
Tuning:
innodb_buffer_pool_size: For a dedicated MySQL server on OVH, allocating 4GB to 8GB (or more, depending on instance type) is common. MonitorInnodb_buffer_pool_read_requestsvs.Innodb_buffer_pool_reads. A hit rate above 99% is desirable.innodb_buffer_pool_instances: Helps reduce contention on the buffer pool mutex. Set to the number of CPU cores or 1 per GB of buffer pool size.
Connection and Thread Handling
Managing client connections and query threads efficiently.
[mysqld] max_connections = 200 ; Adjust based on expected concurrent users and application behavior thread_cache_size = 16 ; Cache threads for reuse
Tuning:
max_connections: The maximum number of simultaneous client connections. Too low will cause connection errors; too high can exhaust server resources. MonitorMax_used_connectionsstatus variable.thread_cache_size: How many idle threads to keep in cache. A value of 16 or 32 is often sufficient.
Query Cache (Deprecated but relevant for older MySQL versions)
Note: The query cache is deprecated in MySQL 5.7 and removed in MySQL 8.0. If you are on an older version, it *might* offer a benefit, but often causes more contention than it solves. For modern deployments, ignore this.
; [mysqld] ; query_cache_type = 1 ; query_cache_size = 64M ; query_cache_limit = 1M
If used, monitor Qcache_hits vs. Qcache_inserts and Qcache_lowmem_prunes. High prunes indicate the cache is too small or too many queries are invalidating it.
Logging and Slow Queries
Identifying slow queries is crucial for optimization. Enable the slow query log.
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 ; Log queries taking longer than 2 seconds log_queries_not_using_indexes = 1 ; Log queries that don't use indexes
Regularly analyze the mysql-slow.log using tools like pt-query-digest to pinpoint problematic SQL statements. Optimize these queries or add appropriate indexes.
OVH Specific Considerations
OVH’s infrastructure often provides dedicated resources, which simplifies some tuning by allowing more aggressive allocation. However, always monitor resource utilization (CPU, RAM, I/O, Network) using OVH’s control panel and standard Linux tools (htop, iotop, nload) to validate your tuning decisions.
For database performance, consider using OVH’s managed database services if available, as they often come pre-tuned and offer high availability. If self-hosting MySQL on an OVH instance, ensure your instance type has sufficient IOPS for your workload.
Network latency can be a factor. Ensure your Nginx and PHP-FPM configurations are optimized to minimize round trips and leverage keep-alive connections effectively.