The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on DigitalOcean for WordPress
Nginx Configuration for WordPress Performance
Optimizing Nginx is crucial for serving WordPress efficiently. We’ll focus on caching, worker processes, and request handling.
Nginx Worker Processes and Connections
The worker_processes directive controls how many worker processes Nginx spawns. Setting this to auto is generally recommended, allowing Nginx to detect the number of CPU cores. The worker_connections directive defines the maximum number of simultaneous connections a worker process can handle. A common starting point is 1024, but this can be increased based on server load and available RAM.
Nginx Caching Strategies
Leveraging Nginx’s FastCGI cache for PHP-FPM output significantly reduces the load on your application and database. This involves defining cache zones and specifying cache behavior.
Configuring FastCGI Cache
First, define your cache zone in the http block. The keys_zone directive names the zone and allocates memory for keys. max_size sets the maximum disk space for the cache, and inactive specifies how long an item can remain in the cache without being accessed before being removed.
Example Nginx http block configuration
http {
# ... other http settings ...
# FastCGI Cache settings
fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=wp_cache:100m max_size=10g inactive=60m use_temp_path=off;
fastcgi_temp_path /var/tmp/nginx/fastcgi_temp; # Ensure this directory exists and is writable by nginx user
# ... other http settings ...
}
Enabling Cache in Server Block
Within your WordPress site’s server block, you’ll enable the cache and define cache zones for specific URIs. The fastcgi_cache_key directive is critical for ensuring unique cache entries. We’ll also set cache validity periods and bypass the cache for logged-in users or specific query parameters.
Example Nginx server block configuration
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_domain.com/public_html;
index index.php index.html index.htm;
# Enable FastCGI cache
set $fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache wp_cache;
fastcgi_cache_valid 200 60m; # Cache for 60 minutes for successful responses
fastcgi_cache_valid 301 302 10m; # Cache redirects 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_bypass $skip_cache;
fastcgi_cache_revalidate on;
fastcgi_cache_lock on;
# Bypass cache for logged-in users or specific cookies
map $http_cookie $skip_cache {
default 0;
"~*wordpress_logged_in" 1;
"~*comment_author" 1;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust to your PHP-FPM version and socket path
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Add cache headers for debugging
add_header X-Cache-Status $upstream_cache_status;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Serve static files directly
location ~* \.(jpg|jpeg|gif|png|ico|css|js|svg|webp)$ {
expires 30d;
access_log off;
add_header Cache-Control "public, no-transform";
}
}
Gunicorn/PHP-FPM Tuning
The application server (Gunicorn for Python-based CMSs, or PHP-FPM for WordPress) is the next critical layer. Proper tuning here ensures that requests are processed efficiently without overwhelming the server.
PHP-FPM Configuration
PHP-FPM’s performance is largely dictated by its process manager settings. The pm directive can be set to static, dynamic, or ondemand. For WordPress, dynamic or ondemand are often good choices, balancing resource usage with responsiveness.
PHP-FPM Pool Configuration (www.conf)
Locate your PHP-FPM pool configuration file (e.g., /etc/php/7.4/fpm/pool.d/www.conf). Key directives to tune include:
pm.max_children: The maximum number of child processes that will be spawned. This is a hard limit.pm.start_servers: The number of child processes to start when PHP-FPM starts.pm.min_spare_servers: The minimum number of idle (spare) processes that should be kept active.pm.max_spare_servers: The maximum number of idle (spare) processes that should be kept active.pm.process_idle_timeout: The number of seconds after which a child process will be killed if it is idle.request_terminate_timeout: The number of seconds after which a script will be killed. Useful for preventing runaway scripts.
Tuning pm.max_children
A common formula to estimate pm.max_children is: (Total RAM - RAM used by OS and other services) / RAM per PHP process. You can monitor RAM usage per PHP process using tools like htop or by analyzing ps aux | grep php-fpm. Start conservatively and increase as needed, monitoring server stability.
Example PHP-FPM Pool Configuration (Dynamic PM)
; /etc/php/7.4/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /var/run/php/php7.4-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 ; Process Manager settings pm = dynamic pm.max_children = 50 ; Adjust based on server RAM and typical load pm.start_servers = 5 ; Initial number of processes pm.min_spare_servers = 2 ; Minimum idle processes pm.max_spare_servers = 10 ; Maximum idle processes pm.process_idle_timeout = 10s ; Timeout for idle processes pm.max_requests = 500 ; Restart child processes after this many requests ; Request termination timeout request_terminate_timeout = 60s ; Other settings chdir = / catch_workers_output = yes ; php_admin_value[error_log] = /var/log/php/php7.4-fpm.log ; php_admin_flag[log_errors] = on ; php_admin_value[memory_limit] = 256M ; Adjust as needed for your WordPress site
PostgreSQL Tuning for WordPress
PostgreSQL, while robust, also benefits from careful tuning, especially under heavy WordPress load. Key areas include shared memory, WAL (Write-Ahead Logging), and connection pooling.
Shared Memory and Buffers
shared_buffers is arguably the most critical parameter. It determines how much memory PostgreSQL uses to cache data. A common recommendation is 25% of system RAM, but this can be adjusted based on your workload and available memory. work_mem controls the amount of memory used for internal sort operations and hash tables before writing to temporary files. For WordPress, especially with complex queries or plugins, increasing this can be beneficial.
Write-Ahead Logging (WAL)
Tuning WAL parameters can improve write performance. wal_buffers is the amount of memory used for WAL data before writing to disk. min_wal_size and max_wal_size control the amount of WAL files that can accumulate before archiving or recycling. Increasing these can reduce I/O pressure during periods of heavy writes.
Connection Pooling
WordPress applications often create many short-lived database connections. Using a connection pooler like PgBouncer can significantly reduce overhead by reusing existing connections. This is especially important on busy sites or when using PHP-FPM with a high number of max_children.
PostgreSQL Configuration (postgresql.conf)
Edit your postgresql.conf file (typically found in /etc/postgresql/[version]/main/). Remember to restart PostgreSQL after making changes.
Example postgresql.conf Tuning
# PostgreSQL configuration file # For more details, see: https://www.postgresql.org/docs/current/runtime-config.html # General Configuration datestyle = 'iso, mdy' timezone = 'UTC' lc_messages = 'en_US.UTF-8' lc_numeric = 'en_US.UTF-8' server_encoding = 'UTF8' # Memory and Buffers shared_buffers = 2GB ; Adjust based on 25% of system RAM work_mem = 64MB ; Increase for complex queries, adjust per workload maintenance_work_mem = 512MB ; For VACUUM, ANALYZE, etc. effective_cache_size = 6GB ; Estimate of total available cache (OS + shared_buffers) # WAL Configuration wal_level = replica ; Or logical if needed for replication wal_buffers = 16MB ; Typically 1/3 of shared_buffers, but capped min_wal_size = 4GB ; Increase to reduce I/O during writes max_wal_size = 16GB ; Increase to reduce I/O during writes checkpoint_completion_target = 0.9 ; Spread checkpoints over time wal_sync_method = fsync ; Or open_sync, fdatasync depending on OS and hardware # Connection Settings max_connections = 100 ; Adjust based on expected concurrent users and PgBouncer usage ; listen_addresses = '*' ; Uncomment and configure if not using a local PgBouncer # Autovacuum settings autovacuum = on autovacuum_max_workers = 3 autovacuum_naptime = 15s autovacuum_vacuum_threshold = 50 autovacuum_analyze_threshold = 50 # Logging log_destination = 'stderr' logging_collector = on log_directory = 'pg_log' log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' log_statement = 'none' ; Consider 'ddl' or 'all' for debugging, but can be verbose log_min_duration_statement = 250ms ; Log queries longer than 250ms log_checkpoints = on log_connections = on log_disconnections = on log_lock_waits = on log_temp_files = 0 ; Log temp files larger than this size (0 to disable) log_autovacuum_min_duration = 10s ; Log autovacuum actions taking longer than 10s
PgBouncer Configuration (Example)
Install and configure PgBouncer. The pgbouncer.ini file is key. We’ll use a transaction pooling mode for WordPress, which is generally safe and efficient.
Example pgbouncer.ini
[databases] ; Database connection string for your WordPress DB your_db_name = host=127.0.0.1 port=5432 dbname=your_db_name user=your_db_user password=your_db_password [pgbouncer] ; Listen address and port listen_addr = 127.0.0.1 listen_port = 6432 ; Pooling mode: session, transaction, or statement ; Transaction mode is generally recommended for WordPress pool_mode = transaction ; Maximum number of clients per server connection default_pool_size = 20 ; Maximum number of server connections for each database max_db_connections = 100 ; Database list (must match the [databases] section) ; The format is "database_name = connection_string" ; Example: ; mydatabase = host=localhost port=5432 dbname=mydb user=myuser password=mypass ; Authentication method (e.g., md5, scram-sha-256, trust) auth_type = md5 auth_file = /etc/pgbouncer/userlist.txt ; Path to userlist file ; Log settings logfile = /var/log/pgbouncer/pgbouncer.log ; loglevel = 2 ; 0=none, 1=error, 2=warn, 3=info, 4=debug ; Other settings ; pidfile = /var/run/pgbouncer/pgbouncer.pid ; admin_users = pgbouncer_admin ; stats_users = stats_user ; server_reset_query = DISCARD ALL;
Example userlist.txt
"your_db_user" "your_db_password"
After configuring PgBouncer, update your WordPress wp-config.php to connect to PgBouncer’s port (e.g., 6432) instead of directly to PostgreSQL.
Monitoring and Iteration
Performance tuning is an iterative process. Regularly monitor your server’s resource utilization (CPU, RAM, I/O), Nginx access logs, PHP-FPM logs, and PostgreSQL logs. Tools like htop, iotop, pg_stat_activity, and Nginx’s stub_status module are invaluable. Use tools like ApacheBench (ab) or k6 for load testing to validate your changes under simulated traffic.