The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Elasticsearch on DigitalOcean for WordPress
Nginx as a High-Performance Reverse Proxy and Static File Server
For WordPress deployments, Nginx excels as a reverse proxy to Gunicorn/PHP-FPM and a highly efficient static file server. Optimizing Nginx is crucial for low latency and high throughput. We’ll focus on key directives for connection handling, caching, and request processing.
Nginx Configuration Tuning
The primary configuration file is typically located at /etc/nginx/nginx.conf. We’ll adjust the http block for global settings and then define specific server blocks for our WordPress site.
Global HTTP Settings
Inside the http block, these directives are paramount:
http {
# ... other settings ...
# Worker connections: Max concurrent connections per worker process.
# Set this to a reasonably high number, e.g., 4096.
# The total max clients = worker_processes * worker_connections.
worker_connections 4096;
# Multi-process model: Use 'threads' for better performance on multi-core CPUs.
# Requires recompiling Nginx with --with-threads. If not compiled with threads,
# 'multi_accept on;' can be used to allow a worker to accept() multiple connections at once.
# For simplicity, we'll assume a standard build and focus on worker_connections.
# multi_accept on; # Uncomment if not using threads and want to accept multiple connections
# Keepalive timeout: How long to keep persistent connections open.
# A moderate value like 65 seconds balances resource usage and client experience.
keepalive_timeout 65;
# Enable gzip compression for text-based assets.
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Compression level (1-9)
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Enable HTTP/2 for faster multiplexing and reduced latency.
# Requires SSL/TLS.
http2 on;
# Client body buffer size: Adjust if you encounter "client intended to send too large body" errors.
# client_body_buffer_size 128k;
# Large client header buffers: For requests with many headers.
# large_client_header_buffers 4 16k;
# ... other settings ...
}
Server Block for WordPress
This server block handles requests for your WordPress site. It includes directives for static file serving, proxying to the application server, and caching.
server {
listen 80;
listen [::]:80;
server_name your-domain.com www.your-domain.com;
# Redirect HTTP to HTTPS (assuming you have SSL configured)
# return 301 https://$host$request_uri;
# --- Static File Serving ---
# Serve static assets directly from Nginx for maximum performance.
# Cache static assets aggressively in the browser.
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; # Optionally disable access logs for static files
try_files $uri =404;
}
# --- WordPress Core & PHP-FPM/Gunicorn Proxy ---
location / {
# Try to serve file directly, then directory, then fall back to proxying.
try_files $uri $uri/ /index.php?$args; # For PHP-FPM
# try_files $uri $uri/ /index.html; # For Gunicorn with SPA-like routing
# Proxy settings for Gunicorn (Python/Flask/Django)
# proxy_pass http://unix:/run/gunicorn.sock; # If using a Unix socket
# proxy_pass http://127.0.0.1:8000; # If using TCP socket
# 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 settings for PHP-FPM
proxy_pass http://unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and path
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_read_timeout 300s; # Increase timeout for potentially long-running PHP scripts
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
# Required for WordPress permalinks to work correctly with PHP-FPM
# This directive is crucial for the try_files directive above.
# It tells Nginx to pass requests for non-existent files to index.php.
# If you are using Gunicorn, this might be handled differently by your framework.
index index.php index.html index.htm;
}
# --- PHP-FPM Specific Configuration ---
# This block is only relevant if proxying to PHP-FPM.
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Adjust the fastcgi_pass directive to match your PHP-FPM socket path.
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and path
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 300s; # Match proxy_read_timeout
fastcgi_connect_timeout 75s;
fastcgi_send_timeout 300s;
}
# --- Security Headers ---
# Add security headers for enhanced protection.
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
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';"; # Example CSP, requires careful tuning
# --- Deny access to sensitive files ---
location ~ /\.ht {
deny all;
}
# --- Logging ---
access_log /var/log/nginx/your-domain.com.access.log;
error_log /var/log/nginx/your-domain.com.error.log;
}
Gunicorn (Python WSGI) Tuning for WordPress/Django/Flask
When using Python frameworks like Django or Flask to serve WordPress (or a custom application), Gunicorn is a popular choice. Its performance is heavily influenced by the number of worker processes and threads.
Gunicorn Worker Processes and Threads
The optimal number of workers depends on your server’s CPU cores and memory. A common starting point is (2 * number_of_cores) + 1 for worker processes. Gunicorn also supports threading within workers.
# Example command to start Gunicorn # Assuming your WSGI application is in 'wsgi.py' in the current directory # and you want 4 worker processes with 2 threads each. # Use a Unix socket for Nginx to connect to. gunicorn --workers 4 --threads 2 --bind unix:/run/gunicorn.sock wsgi:app
Explanation:
--workers 4: Spawns 4 worker processes. Each worker can handle requests independently.--threads 2: Each worker process will spawn 2 threads. This allows a single worker to handle multiple requests concurrently if the application is I/O bound.--bind unix:/run/gunicorn.sock: Gunicorn listens on a Unix domain socket. This is generally faster than TCP sockets for local inter-process communication and is preferred when Nginx is on the same server. Ensure the Nginx user has read/write permissions to this socket file (or the directory it resides in).wsgi:app: Points to your WSGI application object (e.g.,appin a file namedwsgi.py).
Gunicorn Worker Types
Gunicorn supports different worker types. The default is sync, which is a simple synchronous worker. For I/O-bound applications, gevent or eventlet (asynchronous workers) can significantly improve concurrency by using green threads.
# Example using gevent workers gunicorn --worker-class gevent --workers 4 --threads 2 --bind unix:/run/gunicorn.sock wsgi:app
Note: Using asynchronous workers like gevent requires installing the gevent library (`pip install gevent`). They are most effective when your application spends a lot of time waiting for network I/O (e.g., database queries, external API calls).
PHP-FPM Tuning for WordPress
PHP-FPM (FastCGI Process Manager) is the standard way to run PHP applications with Nginx. Its performance is governed by the number of child processes and how they are managed.
PHP-FPM Pool Configuration
The configuration file for PHP-FPM pools is typically found in /etc/php/X.Y/fpm/pool.d/www.conf (replace X.Y with your PHP version, e.g., 8.1). Key directives to tune are:
; /etc/php/8.1/fpm/pool.d/www.conf ; Process manager settings ; 'dynamic' is recommended for most environments. 'static' can offer slightly ; better performance but requires more careful tuning and memory management. pm = dynamic ; If pm.max_children, pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers ; are set, then pm.max_requests is ignored. ; pm.max_requests = 500 ; Number of requests each child process should execute before respawning. ; For 'dynamic' process management: ; Number of child processes to start when PHP-FPM starts. ; A good starting point is (number_of_cores * 2) + 1. ; pm.start_servers = 10 ; Minimum number of child processes to be kept active. ; pm.min_spare_servers = 5 ; Maximum number of child processes to be kept active. ; Should not exceed pm.max_children. ; pm.max_spare_servers = 15 ; Maximum number of children that can be started. ; This is the hard limit. Set based on available RAM. ; A rough estimate: (Total RAM - RAM for OS/Nginx/DB) / Average PHP process memory usage. ; A typical WordPress PHP process might consume 20-50MB. ; If you have 4GB RAM and reserve 1GB for OS/Nginx/DB, you have 3GB (3072MB). ; At 30MB/process, you could potentially support ~100 processes. ; Start conservatively and monitor. pm.max_children = 100 ; Process idle timeout: The number of seconds after which an idle child process will be killed. ; pm.idle_timeout = 10s ; Max execution time for scripts. WordPress often benefits from longer timeouts. ; This is also configurable in php.ini, but setting it here can override it for FPM. ; request_terminate_timeout = 300 ; seconds ; Other important settings (usually in main php.ini, but can be overridden here) ; memory_limit = 256M ; upload_max_filesize = 64M ; post_max_size = 64M ; max_execution_time = 300
Tuning Strategy:
pm = dynamic: This is generally recommended. PHP-FPM will manage the number of child processes based on traffic.pm.max_children: This is the most critical setting. Too high, and you’ll run out of RAM. Too low, and requests will queue up waiting for a process. Monitor your server’s memory usage and the number of active PHP-FPM processes.pm.start_servers,pm.min_spare_servers,pm.max_spare_servers: These help manage the spawning and killing of processes to maintain a responsive pool without excessive overhead.pm.max_requests: Setting this to a moderate value (e.g., 500) helps prevent memory leaks in long-running PHP scripts by periodically respawning processes. If using `dynamic` or `static` `pm` settings, `max_requests` is often less critical than `max_children`.- Timeouts: Ensure
request_terminate_timeoutin the pool config andmax_execution_timeinphp.iniare set high enough for your WordPress site’s needs (e.g., 300 seconds).
After modifying www.conf, restart PHP-FPM: sudo systemctl restart php8.1-fpm.
Elasticsearch Tuning for WordPress Search
For advanced search capabilities in WordPress, Elasticsearch is a powerful backend. Performance tuning involves JVM heap size, indexing settings, and query optimization.
JVM Heap Size Configuration
Elasticsearch runs on the Java Virtual Machine (JVM). Allocating sufficient but not excessive heap memory is vital. The recommended setting is to allocate 50% of system RAM to the JVM heap, up to a maximum of 30-32GB.
# Edit the jvm.options file. Path varies by installation method. # Common paths: # /etc/elasticsearch/jvm.options # /usr/share/elasticsearch/config/jvm.options # Example settings for a server with 16GB RAM: -Xms8g # Initial heap size (e.g., 8GB) -Xmx8g # Maximum heap size (e.g., 8GB) # For servers with more RAM (e.g., 64GB), cap at ~30GB: # -Xms30g # -Xmx30g
After changing jvm.options, restart Elasticsearch: sudo systemctl restart elasticsearch.
Index Settings and Mappings
The way data is indexed significantly impacts search performance. For WordPress, consider:
- Number of Shards and Replicas: For a single-node setup (common on DigitalOcean droplets), set
number_of_replicasto 0. Thenumber_of_shardsshould be chosen based on expected data volume and query load. For WordPress, 1 or 2 shards might suffice initially. - Mapping Optimization: Define explicit mappings for your WordPress data (posts, pages, custom fields) to ensure correct data types and avoid dynamic mapping overhead. Use `keyword` for exact matches and `text` for full-text search.
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 0 // Set to 0 for single-node deployments
}
},
"mappings": {
"properties": {
"post_title": {
"type": "text",
"analyzer": "english"
},
"post_content": {
"type": "text",
"analyzer": "english"
},
"post_author": {
"type": "keyword"
},
"post_date": {
"type": "date"
},
"tags": {
"type": "keyword"
}
// ... other fields
}
}
}
You can apply these settings when creating an index or update them later (though changing shard count requires reindexing).
Query Optimization
Ensure your WordPress search plugin is generating efficient Elasticsearch queries. Avoid overly broad queries and leverage filters where possible. For example, filtering by post type or author is much faster than a full-text search across all documents.
{
"query": {
"bool": {
"must": [
{
"match": {
"post_content": "your search term"
}
}
],
"filter": [
{
"term": {
"post_type": "post"
}
},
{
"range": {
"post_date": {
"gte": "now-30d/d"
}
}
}
]
}
}
}
This query searches post_content for “your search term” while efficiently filtering results to only include posts from the last 30 days and of type “post”.
Monitoring and Iteration
Performance tuning is an ongoing process. Regularly monitor key metrics:
- Nginx: Access logs, error logs,
ngx_http_stub_status_modulefor active connections. - Gunicorn/PHP-FPM: Worker process counts, request latency, error rates, CPU/memory usage.
- Elasticsearch: Cluster health, JVM heap usage, indexing rate, search latency, CPU/memory usage.
- System-wide: CPU utilization, memory usage, disk I/O, network traffic.
Use tools like htop, vmstat, iostat, and Elasticsearch’s monitoring APIs. Adjust configurations iteratively based on observed performance and bottlenecks.