• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on OVH for WordPress

The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on OVH for WordPress

Nginx as a High-Performance Frontend for WordPress

When deploying WordPress on a modern infrastructure, Nginx serves as an exceptionally efficient web server and reverse proxy. Its event-driven, asynchronous architecture excels at handling a high volume of concurrent connections with minimal resource overhead. For WordPress, this translates to faster response times and better scalability. We’ll focus on tuning Nginx for static file serving, SSL termination, and efficient proxying to your PHP-FPM or Gunicorn backend.

Core Nginx Configuration Tuning

The primary Nginx configuration file, typically located at /etc/nginx/nginx.conf, contains global settings that influence worker processes and connection handling. For a production environment, especially on OVH’s robust infrastructure, we can push these limits higher than a typical shared hosting setup.

Start by adjusting the worker_processes directive. Setting this to the number of CPU cores available on your server is a common and effective practice. You can determine the number of cores using nproc.

Determining Worker Processes

nproc

Then, update your nginx.conf:

user www-data;
worker_processes auto; # Or set to the number of CPU cores, e.g., worker_processes 4;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 4096; # Increase based on server capacity and expected load
    multi_accept on;
}

The worker_connections directive defines the maximum number of simultaneous connections that each worker process can open. A value of 4096 is a good starting point, but this can be increased further if your server has ample RAM and you anticipate very high concurrency. The multi_accept on; directive allows a worker to accept as many new connections as possible per event loop iteration.

Optimizing WordPress Site Configuration

Within your WordPress site’s specific Nginx configuration file (e.g., /etc/nginx/sites-available/your-wordpress-site.conf), several directives are crucial for performance. Caching, Gzip compression, and efficient file serving are paramount.

Static File Caching and Compression

server {
    listen 80;
    listen [::]:80;
    server_name your-domain.com www.your-domain.com;

    # Redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your-domain.com www.your-domain.com;

    # SSL Configuration (ensure you have valid certificates)
    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;

    # 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 image/svg+xml;

    # Static File Caching
    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;
        log_not_found off;
    }

    # WordPress specific rules
    root /var/www/your-wordpress-site/public_html;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP-FPM or Gunicorn Proxy Configuration
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # For PHP-FPM
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and socket path
        # For Gunicorn (if using a WSGI app like Flask/Django for WordPress API or headless)
        # fastcgi_pass unix:/path/to/your/gunicorn.sock;
        # fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        # include fastcgi_params;
    }

    # Deny access to sensitive files
    location ~ /\.ht {
        deny all;
    }

    # Other security headers and configurations
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    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'; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self'; media-src 'self'; frame-src 'self'; img-src 'self' data:;"; # Example CSP, adjust as needed
}

Key directives here:

  • gzip on; and related directives: Enable and configure Gzip compression for text-based assets.
  • location ~* \.(jpg|...)$: This block targets static assets. expires 365d; sets a long cache expiry for browsers, and add_header Cache-Control "public, immutable"; instructs browsers and intermediate caches to aggressively cache these files. access_log off; and log_not_found off; reduce I/O for static files.
  • root and index: Define the document root and default index files for your WordPress installation.
  • location / { try_files $uri $uri/ /index.php?$args; }: This is the standard WordPress Nginx rewrite rule, ensuring that requests for non-existent files or directories are passed to index.php for WordPress to handle.
  • location ~ \.php$: This block configures how Nginx communicates with your PHP processor (PHP-FPM in this case). Ensure the fastcgi_pass directive points to the correct PHP-FPM socket.
  • Security headers: Essential for protecting your site against various web vulnerabilities.

Nginx as a Reverse Proxy for Gunicorn/PHP-FPM

When Nginx acts as a reverse proxy, it forwards dynamic requests to a backend application server like Gunicorn (for Python WSGI apps) or PHP-FPM. This offloads the heavy lifting of processing dynamic content from Nginx, allowing Nginx to focus on its strengths: serving static files and managing connections.

Tuning PHP-FPM

PHP-FPM (FastCGI Process Manager) is the de facto standard for running PHP. Its configuration, typically found in /etc/php/8.1/fpm/php-fpm.conf and pool configurations in /etc/php/8.1/fpm/pool.d/www.conf (or a custom pool name), is critical. For high-traffic WordPress sites, tuning the process manager is essential.

Process Manager Settings
; /etc/php/8.1/fpm/pool.d/www.conf

[www]
user = www-data
group = www-data
listen = /var/run/php/php8.1-fpm.sock # Or a TCP/IP socket like 127.0.0.1:9000
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic # Options: static, dynamic, ondemand
pm.max_children = 100 # Adjust based on RAM and CPU
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 10
pm.process_idle_timeout = 10s
pm.max_requests = 500 # Restart worker after this many requests to prevent memory leaks

Explanation of key directives:

  • pm: dynamic is generally recommended. It allows PHP-FPM to scale the number of worker processes based on load. static keeps a fixed number of workers, which can be good for predictable loads but might waste resources. ondemand starts workers only when needed, saving resources but introducing latency on the first request.
  • pm.max_children: This is the most critical setting. It defines the maximum number of PHP-FPM worker processes that can run simultaneously. Setting this too high can exhaust server RAM, leading to OOM killer events. Set it based on your server’s RAM and the typical memory footprint of your WordPress PHP processes. A common starting point is RAM_AVAILABLE / AVERAGE_PHP_PROCESS_MEMORY.
  • pm.start_servers, pm.min_spare_servers, pm.max_spare_servers: These control how PHP-FPM manages its pool of workers when using the dynamic or ondemand process managers.
  • pm.max_requests: Setting this to a reasonable number (e.g., 500-1000) helps prevent memory leaks in long-running PHP scripts or plugins from consuming all available memory over time.

Tuning Gunicorn (for Headless WordPress/API)

If you’re using WordPress in a headless configuration with a Python backend (e.g., Django or Flask) serving the API, Gunicorn is a popular WSGI HTTP Server. Tuning Gunicorn involves managing worker processes and threads.

Gunicorn Worker Configuration
# Example command to start Gunicorn
# Adjust workers and threads based on your server's CPU cores and RAM.
# A common starting point is (2 * CPU_CORES) + 1 workers.
# Threads are useful for I/O-bound tasks.

gunicorn --workers 4 --threads 2 --bind unix:/path/to/your/gunicorn.sock your_wsgi_app:app --timeout 120 --graceful-timeout 120 --log-level info --access-logfile /var/log/gunicorn/access.log --error-logfile /var/log/gunicorn/error.log

Key Gunicorn parameters:

  • --workers: The number of worker processes. A good rule of thumb is (2 * number_of_cpu_cores) + 1.
  • --threads: The number of threads per worker. This is beneficial for I/O-bound operations, allowing a worker to handle multiple requests concurrently if they are waiting on external resources.
  • --bind: The address to bind to. Using a Unix socket (unix:/path/to/socket) is generally more performant than a TCP socket (127.0.0.1:8000) for local communication between Nginx and Gunicorn.
  • --timeout: The maximum time in seconds that a worker can spend on a request before being killed.
  • --graceful-timeout: The maximum time in seconds that Gunicorn will wait for existing requests to finish during a graceful restart.

PostgreSQL Performance Tuning for WordPress

For WordPress sites that rely on PostgreSQL (often via plugins like “WP Migrate DB Pro” or for custom applications), tuning the database is as crucial as tuning the web server. OVH’s VPS and Dedicated servers offer robust PostgreSQL instances, but proper configuration is key.

Key PostgreSQL Configuration Parameters

The primary PostgreSQL configuration file is postgresql.conf. Its location varies by version and OS, but it’s often found in /etc/postgresql/[version]/main/postgresql.conf.

Memory and Caching

# postgresql.conf

# Shared Memory
shared_buffers = 1GB # Adjust based on server RAM. Typically 25% of total RAM.
                   # For 8GB RAM, 2GB is a good starting point.
                   # For 16GB RAM, 4GB. For 32GB+ RAM, 8GB.

# WAL (Write-Ahead Logging)
wal_buffers = 16MB # Default is -1 (auto-tuned based on shared_buffers), but explicit is fine.
wal_writer_delay = 200ms # Default is 200ms.
wal_checkpoint_timeout = 30min # Default is 5min. Increase to reduce I/O spikes.
wal_checkpoint_completion_target = 0.9 # Default is 0.5. Smooths out checkpoint I/O.

# Background Writer
bgwriter_delay = 10ms # Default is 10ms.
bgwriter_lru_maxpages = 1000 # Default is 100. Increase to allow background writer to clean more pages.
bgwriter_lru_multiplier = 1.0 # Default is 1.0.

# Effective Cache Size
# This tells PostgreSQL how much memory is available for OS and other caches.
# It influences the planner's decision on index usage.
# Set to roughly 50-75% of total system RAM.
# For 16GB RAM, this could be 12GB (12288MB).
effective_cache_size = 12GB

# Maintenance Work Memory
# Used for VACUUM, CREATE INDEX, ALTER TABLE ADD FOREIGN KEY.
# Larger values can speed up these operations significantly.
# Set to 5-10% of total RAM, but not more than 2GB for typical WP workloads.
maintenance_work_mem = 512MB

# Work Memory
# Used for sorting and hashing in queries.
# Default is 4MB. Can be increased if you have complex queries or large sorts.
# Be cautious, as this is per-operation, per-connection.
# For WordPress, often not excessively high.
work_mem = 16MB

# Autovacuum
autovacuum = on
autovacuum_max_workers = 3 # Default is 3. Increase if you have many tables and high churn.
autovacuum_naptime = 1min # Default is 1min.
autovacuum_vacuum_threshold = 50 # Default is 50.
autovacuum_analyze_threshold = 50 # Default is 50.

Tuning these parameters requires understanding your server’s RAM and the nature of your WordPress workload:

  • shared_buffers: This is the most important parameter for caching. It’s the amount of memory PostgreSQL uses to cache data. Aim for 25% of your server’s RAM, but don’t exceed 8GB on systems with less than 32GB RAM, as the OS also needs memory.
  • wal_buffers, wal_checkpoint_timeout, wal_checkpoint_completion_target: These tune Write-Ahead Logging. Increasing wal_checkpoint_timeout and wal_checkpoint_completion_target can reduce the frequency and impact of I/O spikes caused by checkpoints, which is beneficial for performance.
  • effective_cache_size: Crucial for the query planner. Setting this accurately helps PostgreSQL decide whether to use indexes or sequential scans. It should reflect the total memory available for caching, including OS caches.
  • maintenance_work_mem: Essential for operations like VACUUM (which WordPress plugins often trigger) and index creation. Increasing this can significantly speed up these maintenance tasks.
  • work_mem: Affects sorting and hashing. For typical WordPress queries, high values are often not needed, but if you encounter slow queries involving sorts, this can be a factor.
  • autovacuum: Ensure autovacuum is enabled. It’s vital for reclaiming space from dead rows and preventing table bloat. Tuning its parameters can ensure it runs efficiently without impacting foreground operations.

Connection Pooling

WordPress, especially with many plugins, can open a large number of database connections. Each connection consumes memory. Using a connection pooler like PgBouncer can significantly reduce overhead and improve performance by reusing existing connections.

PgBouncer Configuration Example

# /etc/pgbouncer/pgbouncer.ini

[databases]
# Format: dbname = host=host port=port user=user password=password
# For local connections, use 'host=/path/to/socket'
wordpress_db = host=/var/run/postgresql port=5432 user=wordpress_user dbname=wordpress_db password=your_db_password

[pgbouncer]
listen_addr = 127.0.0.1 # Or the IP address your application connects to
listen_port = 6432
auth_type = md5 # Or scram-sha-256 for better security
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = session # 'transaction' mode can be problematic for some WP plugins
default_pool_size = 20 # Number of connections per database in the pool
max_client_conn = 1000 # Maximum number of clients that can connect to PgBouncer
default_max_client_conn = 100 # Max clients per database
min_pool_size = 5
pool_timeout = 300 # Seconds to keep idle connections open

# Logging
logfile = /var/log/postgresql/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
log_connections = 0
log_disconnections = 0
log_pooler_errors = 1

And the corresponding userlist.txt:

# /etc/pgbouncer/userlist.txt
# Format: "dbname" "user" "password"
"wordpress_db" "wordpress_user" "your_db_password"

After configuring PgBouncer, you’ll need to adjust your WordPress database connection details (usually in wp-config.php) to point to PgBouncer’s port (e.g., 6432) instead of the direct PostgreSQL port (5432).

Monitoring and Iterative Tuning

Infrastructure tuning is not a one-time event. Continuous monitoring and iterative adjustments are crucial for maintaining optimal performance. On OVH, leverage their monitoring tools and supplement them with standard Linux utilities and application-specific metrics.

Essential Monitoring Tools

  • System Metrics: Use top, htop, vmstat, iostat, and sar to monitor CPU, memory, disk I/O, and network usage. Pay close attention to swap usage (should be minimal) and I/O wait times.
  • Nginx Metrics: Enable Nginx’s stub_status module to get basic connection statistics. For more advanced metrics, consider Prometheus with the nginx-exporter.
  • PHP-FPM Metrics: PHP-FPM also has a status page that can be enabled.
  • PostgreSQL Metrics: Use pg_stat_activity, pg_stat_statements (requires installation and configuration), and tools like pg_top or Prometheus with postgres_exporter.
  • Application Performance Monitoring (APM): Tools like New Relic, Datadog, or open-source alternatives like Jaeger/Prometheus can provide deep insights into WordPress plugin performance, slow database queries, and external API calls.

Iterative Tuning Workflow

  1. Establish a Baseline: Before making any changes, record current performance metrics under typical load.
  2. Identify Bottlenecks: Use monitoring tools to pinpoint the slowest component (CPU, RAM, Disk I/O, Network, Database, Application Code).
  3. Make One Change at a Time: Adjust a single configuration parameter or setting.
  4. Test and Measure: Re-run load tests or observe live traffic to see the impact of the change. Compare against the baseline.
  5. Document: Record the change, its impact, and the new baseline.
  6. Repeat: Continue the cycle until performance targets are met or further improvements yield diminishing returns.

For example, if you observe high CPU usage on your web server, you might first investigate Nginx and PHP-FPM configurations. If database queries are slow, focus on PostgreSQL tuning and connection pooling. If memory is the constraint, review max_children for PHP-FPM or Gunicorn worker/thread counts.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala