The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on Linode for WooCommerce
Nginx as a High-Performance Frontend for WooCommerce
For a WooCommerce site, Nginx serves as an exceptional frontend, efficiently handling static assets, SSL termination, and request routing to your backend application server (Gunicorn for Python/Django or PHP-FPM for PHP). Proper Nginx tuning is paramount for low latency and high throughput.
Nginx Worker Processes and Connections
The `worker_processes` directive dictates how many worker processes Nginx will spawn. A common recommendation is to set this to the number of CPU cores available on your server. `worker_connections` defines the maximum number of simultaneous connections that each worker process can handle. The total maximum connections will be `worker_processes * worker_connections`.
On a typical Linode instance, let’s assume 4 CPU cores. We’ll also set a generous `worker_connections` value, keeping in mind the system’s file descriptor limits.
Tuning nginx.conf
# /etc/nginx/nginx.conf
user www-data;
worker_processes 4; # Set to the number of CPU cores
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on system limits and expected load
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hide Nginx version 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 image/svg+xml;
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# SSL Configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
# Include virtual host configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
After modifying nginx.conf, always test the configuration and reload Nginx:
sudo nginx -t sudo systemctl reload nginx
Optimizing File Descriptor Limits
The `worker_connections` value is ultimately limited by the system’s maximum open file descriptors. Ensure these are set appropriately for Nginx.
Tuning /etc/security/limits.conf
# /etc/security/limits.conf # Increase limits for the nginx user www-data soft nofile 65536 www-data hard nofile 65536 root soft nofile 65536 root hard nofile 65536
You might also need to adjust systemd service limits if Nginx is managed by systemd:
Tuning Systemd Service File
# Edit the Nginx systemd service file sudo systemctl edit nginx.service # Add the following lines to the override file: [Service] LimitNOFILE=65536 LimitNOFILESoft=65536
After these changes, you’ll need to restart Nginx for them to take effect:
sudo systemctl daemon-reload sudo systemctl restart nginx
Caching Strategies with Nginx
Leveraging Nginx’s built-in caching capabilities can significantly reduce load on your backend and database. For WooCommerce, caching static assets and even dynamic content (with careful invalidation) is crucial.
Browser Caching
Set appropriate `Cache-Control` and `Expires` headers for static assets.
# In your Nginx server block or http context
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}
FastCGI/Proxy Caching (for PHP-FPM or Gunicorn)
Nginx can cache responses from your backend application. This is powerful but requires careful consideration for dynamic content like WooCommerce product pages or cart contents.
Configuring Proxy Cache
# In your http context
proxy_cache_path /var/cache/nginx/my_cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
# In your server block, within the location block proxying to your app
location / {
proxy_pass http://your_app_backend; # e.g., http://127.0.0.1:8000 or http://unix:/var/run/php/php7.4-fpm.sock
proxy_cache my_cache;
proxy_cache_valid 200 302 10m; # Cache successful responses for 10 minutes
proxy_cache_valid 404 1m; # Cache 404s for 1 minute
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
# Important for WooCommerce: Do NOT cache POST requests or requests with cookies
proxy_cache_bypass $http_pragma $http_authorization;
proxy_no_cache $http_pragma $http_authorization;
# For PHP-FPM:
# include snippets/fastcgi-php.conf;
# fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
# For Gunicorn:
# 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;
}
The proxy_cache_bypass and proxy_no_cache directives are critical for WooCommerce. They prevent caching of requests that involve user sessions, form submissions, or other dynamic elements that should not be served from a stale cache. You’ll need to carefully define which URIs or request conditions should bypass the cache (e.g., `/cart/`, `/checkout/`, `/my-account/`).
Gunicorn Tuning for Python/Django WooCommerce
When running a Python-based WooCommerce (e.g., using Django with a WooCommerce-like API or a custom solution), Gunicorn is a popular WSGI HTTP Server. Tuning Gunicorn involves adjusting worker processes and threads.
Gunicorn Worker Types and Scaling
Gunicorn offers several worker types:
- Sync Workers (Default): Simple, but each worker can only handle one request at a time.
- Async Workers (e.g., Gevent, Eventlet): Can handle multiple requests concurrently using non-blocking I/O and green threads. This is often preferred for I/O-bound applications like web servers.
- Gevent Workers: A popular choice for I/O-bound tasks.
A common starting point for tuning is to use Gevent workers. The number of workers is typically set to `(2 * Number of CPU Cores) + 1`. The number of threads per worker depends on the application’s I/O patterns.
Gunicorn Configuration Example
You can configure Gunicorn via command-line arguments or a Python configuration file.
Command-line Configuration
# Assuming 4 CPU cores # Using gevent workers for better concurrency gunicorn --workers 9 --worker-class gevent --bind 0.0.0.0:8000 myapp.wsgi:application
Configuration File (e.g., gunicorn_config.py)
# gunicorn_config.py import multiprocessing # Number of worker processes. Typically (2 * num_cores) + 1 workers = (multiprocessing.cpu_count() * 2) + 1 # Use gevent for asynchronous handling worker_class = 'gevent' # Bind to a specific address and port bind = '0.0.0.0:8000' # Or a Unix socket: 'unix:/path/to/your/app.sock' # Other useful settings: # accesslog = '/var/log/gunicorn/access.log' # errorlog = '/var/log/gunicorn/error.log' # loglevel = 'info' # or 'debug', 'warning', 'error', 'critical' # timeout = 30 # seconds # keepalive = 2 # seconds
To use the config file:
gunicorn -c gunicorn_config.py myapp.wsgi:application
PHP-FPM Tuning for PHP WooCommerce
For traditional PHP-based WooCommerce installations, PHP-FPM (FastCGI Process Manager) is the standard. Tuning PHP-FPM involves managing the pool of worker processes that handle PHP requests.
PHP-FPM Process Management Modes
PHP-FPM offers three primary process management modes:
- Static: A fixed number of child processes are spawned when the FPM master process starts. This offers the most predictable performance but can be less efficient if load fluctuates wildly.
- Dynamic: The number of child processes varies dynamically based on load. It starts with a minimum number and spawns more up to a maximum as needed.
- On-Demand: Child processes are spawned only when a request arrives and are killed after a period of inactivity. This saves memory but can introduce latency for the first request after a period of idleness.
For a busy WooCommerce site, dynamic or static modes are generally preferred. Dynamic offers a good balance.
Tuning php-fpm.conf and Pool Configuration
The main configuration file is typically /etc/php/X.Y/fpm/php-fpm.conf (where X.Y is your PHP version), and pool configurations are in /etc/php/X.Y/fpm/pool.d/www.conf.
Example Pool Configuration (www.conf)
; /etc/php/X.Y/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /run/php/phpX.Y-fpm.sock ; Or a TCP socket like 127.0.0.1:9000 ; Process Management (Dynamic Example) pm = dynamic pm.max_children = 50 ; Max number of children at any one time pm.start_servers = 5 ; Number of children when FPM starts pm.min_spare_servers = 2 ; Min number of idle respawns pm.max_spare_servers = 10 ; Max number of idle respawns pm.max_requests = 500 ; Max requests a child process will serve before respawning ; Process Management (Static Example - if load is very predictable) ; pm = static ; pm.max_children = 100 ; Fixed number of children ; Other important settings ; request_terminate_timeout = 120 ; Timeout for script execution (seconds) ; rlimit_files = 1024 ; rlimit_nofile = 65536 ; pm.process_idle_timeout = 10s ; For on-demand mode ; Error Reporting ; log_level = notice ; error_log = /var/log/php/phpX.Y-fpm.log ; access.log = /var/log/php/phpX.Y-fpm-access.log
Tuning `pm.max_children` is critical. It should be set based on your server’s RAM. A common formula is: `pm.max_children = (Total RAM – RAM for OS/other services) / Average RAM per PHP process`. You can monitor RAM usage and PHP-FPM process sizes to fine-tune this.
After changes, reload PHP-FPM:
sudo systemctl reload phpX.Y-fpm
MySQL/MariaDB Tuning for WooCommerce
The database is often the bottleneck for WooCommerce. Optimizing MySQL/MariaDB involves tuning buffer pools, query caches, and connection handling.
Key MySQL/MariaDB Configuration Parameters
These parameters are set in my.cnf or mysqld.cnf (location varies by distribution, often in /etc/mysql/ or /etc/mysql/mysql.conf.d/).
innodb_buffer_pool_size
This is arguably the most important setting for InnoDB. It’s the memory area where InnoDB caches table and index data. For a dedicated database server, setting this to 70-80% of available RAM is common. For a server running both web and DB, this needs to be much lower.
innodb_log_file_size and innodb_log_buffer_size
Larger log files can improve write performance by reducing flushing frequency, but increase recovery time after a crash. `innodb_log_buffer_size` caches log data before writing to disk.
max_connections
The maximum number of simultaneous client connections. WooCommerce and its plugins can open many connections. Set this high enough, but not so high that it exhausts server memory. Monitor `Threads_connected` status.
query_cache_size (Deprecated in MySQL 5.7, Removed in 8.0)
While useful in older versions, the query cache is often a source of contention and is disabled by default in newer MySQL versions. If you are on an older version and have many identical, read-heavy queries, it *might* help, but generally, it’s better to rely on application-level caching and proper indexing.
Example my.cnf Snippet (for a Linode with 8GB RAM, dedicated DB)
# /etc/mysql/my.cnf or /etc/mysql/mysql.conf.d/mysqld.cnf [mysqld] # General Settings user = mysql pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql log-error = /var/log/mysql/error.log # log_queries_not_using_indexes = 1 # Useful for debugging, but can be noisy # InnoDB Settings innodb_buffer_pool_size = 6G ; ~75% of 8GB RAM, adjust based on actual usage innodb_log_file_size = 512M ; Adjust based on write load innodb_log_buffer_size = 16M innodb_flush_method = O_DIRECT ; Recommended for Linux with hardware RAID/SSDs innodb_flush_log_at_trx_commit = 1 ; Strict ACID compliance (can be 2 for performance if data loss risk is acceptable) innodb_file_per_table = 1 ; Recommended for better management # Connection Settings max_connections = 200 ; Adjust based on application needs and server RAM # thread_cache_size = 16 ; Cache threads for reuse # MyISAM Settings (if you still use MyISAM tables, though InnoDB is preferred) # key_buffer_size = 128M # Temporary Tables # tmp_table_size = 64M # max_heap_table_size = 64M # Sort Buffers # sort_buffer_size = 2M # read_rnd_buffer_size = 2M
After modifying the configuration, restart MySQL:
sudo systemctl restart mysql
Database Indexing and Query Optimization
Configuration tuning is only part of the story. Slow queries are a major performance killer for WooCommerce. Regularly analyze and optimize your database queries.
Identifying Slow Queries
Enable the slow query log in MySQL:
# In my.cnf under [mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 ; Log queries taking longer than 2 seconds # log_queries_not_using_indexes = 1 ; Uncomment to log queries without indexes
Restart MySQL and then analyze the slow query log file. Tools like pt-query-digest from the Percona Toolkit are invaluable for summarizing these logs.
sudo apt-get install percona-toolkit # Or equivalent for your distro pt-query-digest /var/log/mysql/mysql-slow.log > /var/log/mysql/mysql-slow-report.txt
Adding Indexes
Use `EXPLAIN` to understand query execution plans and identify missing indexes. For WooCommerce, common areas needing indexing include `wp_posts` (especially `post_type`, `post_status`), `wp_postmeta`, `wp_options`, and order-related tables.
-- Example: Analyzing a query EXPLAIN SELECT * FROM wp_posts WHERE post_type = 'product' AND post_status = 'publish'; -- Example: Adding an index if needed ALTER TABLE wp_posts ADD INDEX idx_post_type_status (post_type, post_status);
Be cautious with adding too many indexes, as they consume disk space and slow down write operations (INSERT, UPDATE, DELETE). Focus on indexes that significantly speed up frequent read queries.
Monitoring and Iteration
Performance tuning is an ongoing process. Implement robust monitoring to track key metrics and identify regressions or new bottlenecks.
Key Metrics to Monitor
- Nginx: Active connections, requests per second, error rates (4xx, 5xx), cache hit/miss ratios.
- Gunicorn/PHP-FPM: Worker utilization, request queue length, response times, memory usage per worker.
- MySQL: Query throughput, slow queries, connection usage, buffer pool hit rate, disk I/O, CPU usage.
- System: CPU load, RAM usage, disk I/O, network traffic.
Tools like Prometheus with Grafana, Datadog, New Relic, or even basic system tools like htop, iotop, and MySQL’s `SHOW GLOBAL STATUS` are essential.
Continuously analyze your monitoring data, identify slow points, adjust configurations incrementally, and re-test. This iterative approach ensures your WooCommerce site remains performant under varying loads.