The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on Linode for WordPress
Nginx as a High-Performance Frontend for WordPress
When deploying WordPress with a modern stack, Nginx serves as an exceptional frontend, efficiently handling static assets, SSL termination, and request routing to your application server (Gunicorn for Python-based CMS, or PHP-FPM for traditional PHP). Tuning Nginx is paramount for maximizing throughput and minimizing latency.
Core Nginx Configuration Tuning
The primary configuration file, typically located at /etc/nginx/nginx.conf, contains global settings. Key directives to optimize include:
worker_processes: Set this to the number of CPU cores available. On a Linode instance, this is easily discoverable.worker_connections: The maximum number of simultaneous connections each worker process can handle. A common starting point is1024or higher, depending on expected load.multi_accept: Set toonto allow workers to accept multiple connections at once.keepalive_timeout: Controls how long an idle HTTP connection will remain open. A value between15and65seconds is typical.sendfile: Set toonto enable efficient file transfer by copying data directly from the kernel’s page cache to the socket.tcp_nopushandtcp_nodelay: Set toonto optimize packet transmission.
Here’s an example of a tuned nginx.conf snippet:
worker_processes auto; # Or set to the number of CPU cores
worker_connections 4096; # Adjust based on expected load
multi_accept on;
events {
use epoll; # For Linux systems
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000; # Max requests per keepalive connection
# Gzip compression for text-based assets
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;
# Caching for static assets
location ~* \.(jpg|jpeg|gif|png|webp|svg|css|js|ico|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# ... other server blocks ...
}
WordPress-Specific Nginx Configuration
For WordPress, the server block needs careful configuration to route requests appropriately and handle PHP processing. If you’re using PHP-FPM, Nginx will proxy requests to it. If you’re using Gunicorn (e.g., with a Python-based WordPress alternative or a custom setup), the proxy configuration will differ.
Nginx with PHP-FPM
This is the most common setup for traditional PHP WordPress. The location ~ \.php$ block is critical.
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/html/wordpress; # Adjust to your WordPress installation path
index index.php index.html index.htm;
# SSL Configuration (if applicable)
# 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;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust PHP version and socket path
# Or using TCP/IP:
# fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Static file caching (already defined globally, but can be overridden)
location ~* \.(jpg|jpeg|gif|png|webp|svg|css|js|ico|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
Nginx with Gunicorn (for Python-based CMS)
If you’re using a Python framework like Django or Flask for your CMS, Gunicorn will be your WSGI HTTP Server. Nginx will proxy requests to Gunicorn, typically via a Unix socket or TCP port.
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /path/to/your/python/cms/public; # Adjust to your CMS's static file root
index index.html index.htm;
# Static file serving (handled by Nginx for performance)
location /static/ {
alias /path/to/your/python/cms/static/; # Adjust path
expires 30d;
add_header Cache-Control "public, no-transform";
}
location /media/ {
alias /path/to/your/python/cms/media/; # Adjust path
expires 30d;
add_header Cache-Control "public, no-transform";
}
location / {
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 to Gunicorn via Unix Socket
proxy_pass http://unix:/run/gunicorn.sock; # Adjust socket path
# Or proxy to Gunicorn via TCP/IP
# proxy_pass http://127.0.0.1:8000;
}
}
Gunicorn Tuning for WordPress (Python-based)
Gunicorn (Green Unicorn) is a Python WSGI HTTP Server commonly used to run Python web applications. Tuning Gunicorn involves adjusting its worker processes and threads to match your server’s resources and application’s I/O characteristics.
Gunicorn Worker Configuration
The most impactful settings are:
--workers: The number of worker processes. A common recommendation is(2 * CPU_CORES) + 1.--threads: The number of threads per worker. This is crucial for I/O-bound applications. For WordPress-like workloads (which can be I/O bound due to database queries and file access), using threads can significantly increase concurrency without the overhead of additional processes.--worker-connections: (Deprecated in newer Gunicorn versions, replaced by threads) Maximum number of clients a worker can serve simultaneously.--worker-class: The type of worker.syncis the default and simplest.geventoreventletare asynchronous workers that can handle more concurrent connections with fewer resources, especially beneficial for I/O-bound tasks.
Here’s an example of how you might start Gunicorn:
# Example for a 4-core CPU server, using gevent for async I/O gunicorn --workers 9 --worker-class gevent --threads 2 --bind unix:/run/gunicorn.sock your_project.wsgi:application --log-level info --log-file /var/log/gunicorn/your_project.log
Note: The --threads option is only applicable when using worker classes that support threading (like sync). Asynchronous workers like gevent manage concurrency differently, and the concept of threads per worker is less direct. For gevent, focus on the number of workers and ensure your application code is gevent-compatible.
Gunicorn Timeout and Keep-Alive
Adjusting timeouts is important to prevent workers from being killed prematurely during long-running operations, but also to free up workers from stuck requests.
--timeout: The number of seconds to wait for a worker to respond. Default is 30. Increase if you have known long-running operations, but be cautious.--keep-alive: The number of seconds to wait for a new request on a keep-alive connection.
# Example with increased timeout gunicorn --workers 9 --worker-class gevent --threads 2 --timeout 120 --bind unix:/run/gunicorn.sock your_project.wsgi:application
PHP-FPM Tuning for WordPress
PHP-FPM (FastCGI Process Manager) is the de facto standard for running PHP applications with Nginx. Its performance is heavily influenced by its process management and resource allocation.
PHP-FPM Pool Configuration
PHP-FPM pools are configured in files typically found in /etc/php/X.Y/fpm/pool.d/www.conf (where X.Y is your PHP version). Key directives include:
pm: Process manager control. Options arestatic,dynamic, andondemand.static: Keeps a fixed number of child processes running. Good for predictable loads and dedicated servers.dynamic: Starts processes as needed, up to a maximum.ondemand: Starts processes only when a request comes in.
pm.max_children: The maximum number of child processes that will be spawned. This is the most critical setting for memory usage.pm.start_servers: The number of child processes to start when PHP-FPM starts. (Fordynamic)pm.min_spare_servers: The minimum number of “spare” (idle) processes. (Fordynamic)pm.max_spare_servers: The maximum number of “spare” (idle) processes. (Fordynamic)pm.max_requests: The number of requests each child process will execute before respawning. This helps prevent memory leaks.
Recommendation: For WordPress, a static process manager is often preferred for consistent performance, especially on servers with sufficient RAM. If memory is constrained, dynamic can be a good compromise.
; Example for a server with 8GB RAM and 16 CPU cores ; Using static process management for predictable performance pm = static pm.max_children = 150 ; Adjust based on available RAM and typical WordPress memory usage per process pm.max_requests = 500 ; Helps mitigate memory leaks over time ; If using dynamic, consider these: ; pm = dynamic ; pm.max_children = 150 ; pm.start_servers = 10 ; pm.min_spare_servers = 5 ; pm.max_spare_servers = 20 ; pm.max_requests = 500 ; Other important settings request_terminate_timeout = 60s ; Timeout for individual requests listen.owner = www-data listen.group = www-data listen.mode = 0660 listen = /var/run/php/php7.4-fpm.sock ; Ensure this matches your Nginx config user = www-data group = www-data
After modifying www.conf, you must restart PHP-FPM:
sudo systemctl restart php7.4-fpm # Adjust PHP version as needed
PHP Settings for WordPress
Beyond FPM configuration, core PHP settings in php.ini (e.g., /etc/php/X.Y/fpm/php.ini) also impact performance:
memory_limit: Ensure this is sufficient for WordPress and its plugins (e.g.,256Mor512M).max_execution_time: The maximum time in seconds a script is allowed to run. Default is 30. Increase cautiously if needed for specific operations.upload_max_filesizeandpost_max_size: Important for media uploads.opcache.enableandopcache.memory_consumption: Essential for performance. Ensure OPcache is enabled and has adequate memory allocated (e.g.,128Mor256M).
[OPcache] opcache.enable=1 opcache.enable_cli=1 opcache.memory_consumption=256 ; MB opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.revalidate_freq=60 opcache.validate_timestamps=1 opcache.save_comments=1 opcache.enable_file_override=0 [PHP] memory_limit = 512M max_execution_time = 120 upload_max_filesize = 64M post_max_size = 64M
PostgreSQL Tuning for WordPress Database
PostgreSQL is a robust database system. Optimizing its configuration, particularly postgresql.conf, is crucial for WordPress performance, as the database is often the bottleneck.
Key PostgreSQL Configuration Parameters
Locate your postgresql.conf file (path varies by OS and version, often /etc/postgresql/X.Y/main/postgresql.conf). Key parameters to tune:
shared_buffers: The amount of memory dedicated to PostgreSQL’s shared buffer cache. A common starting point is 25% of system RAM, but can be pushed higher (up to 40-50%) on dedicated database servers.work_mem: The amount of memory used for internal sort operations and hash tables before disk spills occur. This is per operation, so be conservative.maintenance_work_mem: Memory used for maintenance operations likeVACUUM,CREATE INDEX, andALTER TABLE.effective_cache_size: An estimate of how much memory is available for disk caching by the operating system and PostgreSQL’s shared buffers.wal_buffers: Memory for WAL (Write-Ahead Logging) data.checkpoint_completion_target: Spreads WAL checkpointing over time, reducing I/O spikes.max_connections: Maximum number of concurrent connections.
Here’s an example snippet for a Linode instance with 16GB RAM:
# Example for a 16GB RAM server shared_buffers = 4GB ; ~25% of RAM work_mem = 64MB ; Adjust based on query complexity and RAM maintenance_work_mem = 512MB ; For VACUUM, index creation etc. effective_cache_size = 12GB ; OS cache + shared_buffers wal_buffers = 16MB checkpoint_completion_target = 0.9 max_wal_size = 4GB ; Replaces wal_level and related settings in newer versions min_wal_size = 1GB max_connections = 200 ; Adjust based on application needs and server resources random_page_cost = 1.1 ; Lower for SSDs seq_page_cost = 1.0
After modifying postgresql.conf, you must reload or restart the PostgreSQL service:
sudo systemctl reload postgresql # Or restart if reload is not sufficient
PostgreSQL Vacuuming and Indexing
Regular maintenance is critical. WordPress databases can become bloated with post revisions, transients, and orphaned metadata. Automated vacuuming and proper indexing are key.
- Autovacuum: Ensure autovacuum is enabled and tuned. The default settings are often too conservative. You might need to adjust
autovacuum_vacuum_scale_factorandautovacuum_analyze_scale_factor. - Index Analysis: Regularly analyze your database schema for missing or redundant indexes. Tools like
pg_stat_statementsandEXPLAIN ANALYZEare invaluable. - WordPress Plugins: Be mindful of plugins that create excessive database load or bloat (e.g., poorly optimized theme options, analytics plugins, or revision management plugins). Consider using plugins like WP-Optimize for database cleanup.
Monitoring and Iterative Tuning
Performance tuning is not a one-time event. Continuous monitoring and iterative adjustments are essential. Use tools like:
- Nginx:
nginx -s reload,/var/log/nginx/access.log,/var/log/nginx/error.log, Nginx Amplify, or Prometheus/Grafana with the Nginx exporter. - PHP-FPM: Status page (if enabled),
/var/log/phpX.Y-fpm.log, New Relic, or Blackfire.io. - PostgreSQL:
pg_stat_activity,pg_stat_statements,EXPLAIN ANALYZE, pgBadger, Prometheus/Grafana with the node_exporter and postgres_exporter. - System:
top,htop,vmstat,iostat, Linode’s built-in monitoring.
Start with conservative settings, monitor the impact, and gradually increase limits or adjust parameters based on observed performance metrics and resource utilization. Always test changes in a staging environment before deploying to production.