The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on Linode for WordPress
Nginx Configuration for WordPress Performance
Optimizing Nginx is crucial for serving WordPress efficiently. We’ll focus on caching, worker processes, and connection handling.
Worker Processes and Connections
The worker_processes directive should ideally be set to the number of CPU cores available. worker_connections dictates the maximum number of simultaneous connections a worker can handle. A common starting point is to set worker_connections to a value that, when multiplied by worker_processes, exceeds your expected peak concurrent users, considering other processes on the server.
Example Nginx Configuration Snippet
worker_processes auto; # Or set to the number of CPU cores
worker_connections 4096; # Adjust based on server load and RAM
events {
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Important for security
# 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;
# Buffers
client_body_buffer_size 10K;
client_header_buffer_size 1K;
large_client_header_buffers 2 4K;
output_buffers 1 32K;
post_process_buffer_size 16K;
# Caching
proxy_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=wordpress:10m max_size=10g inactive=60m use_temp_path=off;
proxy_temp_path /var/tmp/nginx/proxy_temp; # Ensure this directory exists and is writable by nginx user
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;
# WordPress specific rules
location / {
try_files $uri $uri/ /index.php?$args;
}
# Serve static files directly
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
# PHP-FPM configuration
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_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Caching for PHP-FPM output
proxy_cache wordpress;
proxy_cache_valid 200 302 10m; # Cache for 10 minutes
proxy_cache_valid 404 1m;
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
# Deny access to sensitive files
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
location ~ /\.ht {
deny all;
}
}
}
Caching Implementation
Nginx’s built-in proxy caching is highly effective. The proxy_cache_path directive defines the location and parameters for the cache. keys_zone creates a shared memory zone to store cache keys, and max_size limits the total cache size. inactive specifies how long an item can remain in the cache without being accessed before it’s removed.
Within the server block, proxy_cache wordpress; enables caching for the defined zone. proxy_cache_valid sets the duration for which specific HTTP response codes are cached. The X-Cache-Status header is invaluable for debugging cache hits and misses.
Gunicorn/PHP-FPM Tuning for WordPress
The choice between Gunicorn (for Python-based frameworks often used with WordPress via plugins or headless setups) and PHP-FPM (the standard for traditional PHP WordPress) dictates the tuning approach. We’ll cover both.
PHP-FPM Optimization
PHP-FPM’s performance hinges on its process manager settings. The pm directive can be set to static, dynamic, or ondemand. For most WordPress sites on Linode, dynamic offers a good balance between resource utilization and responsiveness.
Dynamic Process Manager Settings
pm.max_children: The maximum number of child processes that will be spawned. This is a critical parameter. Too low, and requests will queue up. Too high, and you’ll exhaust server memory. A good starting point is (Total RAM - RAM for OS/Nginx/MySQL) / Average PHP-FPM Child Memory Usage.
pm.start_servers: The number of child processes to start when PHP-FPM starts.
pm.min_spare_servers: The minimum number of idle spare servers to maintain.
pm.max_spare_servers: The maximum number of idle spare servers to maintain.
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 should execute before respawning. This helps mitigate memory leaks.
Example PHP-FPM Pool Configuration (www.conf)
This configuration is typically found in /etc/php/8.1/fpm/pool.d/www.conf (adjust version and path as needed).
; Start a new pool [www] ; Unix user/group of processes user = www-data group = www-data ; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'ip.add.re.ss:port' - to listen on TCP socket, e.g. 127.0.0.1:9000 ; '::1:port' - to listen on TCP socket, e.g. [::1]:9000 ; '/path/to/socket.sock' - to listen on Unix socket listen = /var/run/php/php8.1-fpm.sock ; Listen permissions listen.owner = www-data listen.group = www-data listen.mode = 0660 ; Process manager settings pm = dynamic pm.max_children = 100 ; Adjust based on server RAM and typical child process size pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.process_idle_timeout = 10s pm.max_requests = 500 ; Helps prevent memory leaks ; Other useful settings request_terminate_timeout = 60s ; Timeout for script execution ; rlimit_files = 1024 ; Max open files per process ; rlimit_core = 0 ; Core dump size
Gunicorn Optimization (for Python/Headless WordPress)
If you’re using Gunicorn to serve a Python application that interacts with WordPress (e.g., via REST API), tuning involves worker types and counts.
Worker Types and Count
Gunicorn offers several worker types: sync (synchronous, default), eventlet, gevent, and tornado. For I/O-bound applications, asynchronous workers like gevent or eventlet are often preferred. The number of workers is typically set to (2 * Number of CPU Cores) + 1 as a starting point.
Example Gunicorn Command Line
gunicorn --workers 4 --worker-class gevent --bind 0.0.0.0:8000 your_app.wsgi:application
In this example:
--workers 4: Sets the number of worker processes. Adjust based on CPU cores.--worker-class gevent: Uses the gevent asynchronous worker class.--bind 0.0.0.0:8000: Binds Gunicorn to all network interfaces on port 8000.your_app.wsgi:application: Points to your application’s WSGI entry point.
MySQL Tuning for WordPress
Database performance is often a bottleneck. Tuning MySQL involves adjusting buffer pools, query cache (though often disabled in modern MySQL versions), and connection limits.
Key MySQL Configuration Parameters
innodb_buffer_pool_size: This is arguably the most critical setting for InnoDB. It’s the memory area where InnoDB caches table and index data. Set it to 50-80% of your available RAM on a dedicated database server. On a shared Linode, be more conservative.
innodb_log_file_size and innodb_log_buffer_size: Larger log files can improve write performance but increase recovery time. A common recommendation is 256MB to 1GB for innodb_log_file_size.
max_connections: The maximum number of simultaneous client connections. WordPress sites can sometimes open many connections, especially with poorly written plugins. Monitor your actual usage and set this appropriately, but avoid excessively high values that can lead to resource exhaustion.
query_cache_size and query_cache_type: The query cache is often disabled in MySQL 5.7+ and removed in MySQL 8.0 due to scalability issues. If you are on an older version and have a read-heavy workload with many identical queries, it might offer a benefit, but generally, it’s better to rely on application-level or Nginx caching.
Example MySQL Configuration (my.cnf)
This snippet would be added to your my.cnf file (e.g., /etc/mysql/mysql.conf.d/mysqld.cnf or /etc/my.cnf).
[mysqld] # General Settings user = mysql pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock port = 3306 basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp lc_messages_dir = /usr/share/mysql lc_messages = en_US skip-external-locking # InnoDB Settings innodb_file_per_table = 1 innodb_flush_method = O_DIRECT innodb_buffer_pool_size = 1G # Adjust based on available RAM (e.g., 50-70% of RAM on dedicated DB server) innodb_log_file_size = 512M # Adjust based on write load innodb_log_buffer_size = 64M innodb_flush_log_at_trx_commit = 1 # For ACID compliance, 2 for slightly better performance but less safety # Connection Settings max_connections = 150 # Adjust based on monitoring and application needs # thread_cache_size = 16 # Cache threads for reuse # Query Cache (Generally disable for modern MySQL versions) # query_cache_type = 0 # query_cache_size = 0 # Other performance tuning # table_open_cache = 2000 # table_definition_cache = 1000 # sort_buffer_size = 2M # join_buffer_size = 2M # read_rnd_buffer_size = 1M # tmp_table_size = 64M # max_heap_table_size = 64M
Monitoring and Iteration
Tuning is an iterative process. Use tools like:
- Nginx:
ngx_http_stub_status_modulefor connection stats,X-Cache-Statusheader, and access/error logs. - PHP-FPM: The FPM status page (requires configuration) and PHP’s built-in profiling tools (e.g., Xdebug, Blackfire.io).
- MySQL:
SHOW GLOBAL STATUS;,SHOW ENGINE INNODB STATUS;, slow query log, and tools like Percona Monitoring and Management (PMM). - System:
top,htop,vmstat,iostatto monitor CPU, memory, I/O, and network usage.
Start with conservative settings, monitor performance under load, and gradually adjust parameters. Document all changes and their impact.