The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on Linode for Shopify
Nginx as a High-Performance Frontend for Shopify Applications
When deploying a custom Shopify backend or a headless architecture on Linode, Nginx serves as the critical front-facing web server. Its efficiency in handling static assets, SSL termination, and request routing is paramount. For optimal performance, we’ll focus on tuning worker processes, connection limits, and caching strategies.
Worker Processes and Connections
The `worker_processes` directive controls how many worker processes Nginx spawns. A common recommendation is to set this to the number of CPU cores available. The `worker_connections` directive sets the maximum number of simultaneous connections that each worker process can handle. The total maximum connections will be `worker_processes * worker_connections`.
On a Linode instance, identify the number of CPU cores using `nproc` or by checking `/proc/cpuinfo`. For a typical 4-core Linode, a good starting point is:
worker_processes 4;
events {
worker_connections 4096; # Adjust based on available RAM and expected load
multi_accept on;
}
The `multi_accept on;` directive allows a worker to accept as many new connections as possible when an event loop indicates that new connections are available. This can improve throughput under heavy load.
HTTP Request Buffering and Timeouts
Nginx buffers client requests. If a request is larger than the buffer, it’s written to a temporary file. For API-heavy applications, tuning these can prevent unnecessary disk I/O. The `client_body_buffer_size` should be sufficient for typical request bodies. `client_max_body_size` limits the total size of the client request body.
Timeouts are crucial to prevent resource exhaustion from slow or stuck clients. `client_header_timeout`, `client_body_timeout`, and `send_timeout` should be set to reasonable values, typically between 30-60 seconds for web applications.
http {
# ... other http directives ...
client_body_buffer_size 128k;
client_max_body_size 10m; # Adjust based on expected file uploads
client_header_timeout 60s;
client_body_timeout 60s;
send_timeout 60s;
# ... other http directives ...
}
Gzip Compression and Caching
Enabling Gzip compression significantly reduces bandwidth usage and improves page load times. Configure it to compress dynamic content served by your backend application.
http {
# ... other http directives ...
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;
# Browser caching for static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public";
}
# ... other http directives ...
}
The `gzip_types` directive specifies MIME types to compress. `gzip_comp_level` balances compression ratio with CPU usage. For static assets, setting an `expires` header and `Cache-Control` allows browsers to cache them effectively.
Optimizing Gunicorn for Python/Django/Flask Applications
Gunicorn (Green Unicorn) is a popular WSGI HTTP Server for Python web applications. Its performance is heavily influenced by the number of worker processes and the worker type.
Worker Processes and Types
Gunicorn’s `workers` setting determines the number of worker processes. A common heuristic is `(2 * number_of_cores) + 1`. For I/O-bound applications, increasing this can be beneficial. The `worker_class` dictates how workers handle requests. `sync` is the default and simplest, but `gevent` or `eventlet` (asynchronous workers) can offer better concurrency for I/O-bound tasks by using green threads.
To deploy a Gunicorn application behind Nginx on a 4-core Linode:
# Example command to start Gunicorn
gunicorn --workers 9 \
--worker-class gevent \
--bind 0.0.0.0:8000 \
your_project.wsgi:application
If using `gevent` or `eventlet`, ensure you have installed the respective libraries (`pip install gevent` or `pip install eventlet`). For CPU-bound tasks, the `sync` worker class with a higher number of workers might be more suitable, but be mindful of the GIL.
Timeouts and Keep-Alive
Gunicorn’s `timeout` setting defines the maximum time a worker can spend processing a request before it’s killed and restarted. This prevents hung requests from blocking workers. `keepalive` controls the number of requests a worker can handle before being recycled.
gunicorn --workers 9 \
--worker-class gevent \
--bind 0.0.0.0:8000 \
--timeout 120 \
--keepalive 100 \
your_project.wsgi:application
A `timeout` of 120 seconds is a reasonable starting point for web applications, allowing for longer-running API calls. `keepalive` can be set high to reduce worker startup overhead, but a moderate value prevents memory leaks from accumulating over many requests.
Tuning PHP-FPM for PHP-based Backends
For PHP applications, PHP-FPM (FastCGI Process Manager) is the standard. Its configuration directly impacts how PHP requests are handled by the web server (Nginx).
Process Manager Settings
PHP-FPM offers several process management strategies: `static`, `dynamic`, and `ondemand`. `dynamic` is often a good balance, allowing FPM to scale the number of workers based on load within defined limits.
Edit your PHP-FPM pool configuration file (e.g., `/etc/php/8.1/fpm/pool.d/www.conf` or similar, depending on your PHP version and OS):
; Choose one of the process management modes: ; pm = static ; pm = dynamic pm = dynamic ; If pm is 'dynamic', these are the pm settings: ; pm.max_children: The maximum number of children that can be spawned. ; pm.start_servers: The number of children initially created on startup. ; pm.min_spare_servers: The desired number of minimal idle supervisors. ; pm.max_spare_servers: The desired number of maximal idle supervisors. ; pm.process_idle_timeout: The number of seconds after which a child process will be killed when idle. ; pm.max_requests: The number of requests each child process will execute before reexecuting. pm.max_children = 100 ; Adjust based on RAM and expected concurrency pm.start_servers = 5 ; Initial workers pm.min_spare_servers = 2 ; Minimum idle workers pm.max_spare_servers = 5 ; Maximum idle workers pm.process_idle_timeout = 10s ; Kill idle workers after 10 seconds pm.max_requests = 500 ; Recycle workers after 500 requests
The `pm.max_children` is the most critical. It should be set such that the total memory usage of all PHP-FPM processes does not exceed available RAM. A common approach is to estimate the average memory footprint per PHP-FPM process (e.g., 20-50MB) and divide available RAM by this figure. `pm.max_requests` helps prevent memory leaks by recycling worker processes.
Nginx and PHP-FPM Communication
Ensure Nginx is configured to communicate with PHP-FPM efficiently, typically via a Unix socket for lower latency or a TCP port.
server {
# ... other server directives ...
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
# With php-fpm (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
}
# ... other server directives ...
}
The `fastcgi_pass` directive points to your PHP-FPM socket or TCP address. The `snippets/fastcgi-php.conf` file (common in Debian/Ubuntu) contains essential FastCGI parameters.
MySQL Performance Tuning for Shopify Data
Shopify applications often rely on robust database performance. Tuning MySQL, particularly the InnoDB storage engine, is crucial for handling product catalogs, customer data, and order information.
InnoDB Buffer Pool
The `innodb_buffer_pool_size` is the most critical InnoDB setting. It caches data and indexes for InnoDB tables. Ideally, this should be set to 50-75% of your Linode’s available RAM, leaving enough for the OS and other processes. On a 16GB RAM Linode, you might set this to 10-12GB.
[mysqld] innodb_buffer_pool_size = 10G # Example for a 16GB RAM server innodb_buffer_pool_instances = 8 # Typically 1 instance per GB of buffer pool, up to 16
`innodb_buffer_pool_instances` helps reduce contention on multi-core systems by dividing the buffer pool into multiple regions. Set it to a power of 2, up to 16, based on the buffer pool size.
Connection Handling and Threading
`max_connections` determines the maximum number of simultaneous client connections. Set this based on your application’s needs and server resources. Too high can lead to resource exhaustion.
[mysqld] max_connections = 500 # Adjust based on application and server load thread_cache_size = 100 # Cache threads for reuse innodb_thread_concurrency = 0 # 0 means unlimited, or set to ~2x cores
`thread_cache_size` helps by reusing threads instead of creating new ones for each connection, reducing overhead. `innodb_thread_concurrency` limits the number of threads that can be active simultaneously within InnoDB. Setting it to 0 allows InnoDB to manage it dynamically, which is often fine, but explicitly setting it can sometimes help on highly contended systems.
Query Cache (Deprecated/Removed) and Logging
The MySQL query cache is deprecated in MySQL 5.7 and removed in MySQL 8.0. Do not enable it. Focus instead on optimizing queries and ensuring proper indexing.
Enable the slow query log to identify inefficient queries. Set `long_query_time` to a low value (e.g., 1 or 2 seconds) to capture queries that take longer than that.
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 log_queries_not_using_indexes = 1 # Optional: log queries that don't use indexes
Regularly analyze the slow query log using tools like `mysqldumpslow` or `pt-query-digest` to pinpoint and optimize problematic SQL statements.
Putting It All Together: Linode Configuration and Monitoring
Deploying these configurations on Linode requires careful attention to system resources and ongoing monitoring. After applying changes to Nginx, PHP-FPM, or MySQL configuration files, remember to restart or reload the respective services:
# Reload Nginx sudo systemctl reload nginx # Restart PHP-FPM (adjust service name for your version) sudo systemctl restart php8.1-fpm # Restart MySQL sudo systemctl restart mysql
Monitoring is key. Utilize Linode’s built-in resource monitoring to track CPU, RAM, disk I/O, and network traffic. For deeper insights:
- Nginx: Use `stub_status` module for active connections and requests.
- Gunicorn: Monitor worker processes and request latency.
- PHP-FPM: Check the FPM status page for active processes and request counts.
- MySQL: Use `SHOW GLOBAL STATUS;` and `SHOW ENGINE INNODB STATUS;` for real-time metrics.
Tools like Prometheus with Grafana, or Datadog, can provide comprehensive, long-term performance visibility. Regularly review these metrics to identify bottlenecks and proactively adjust configurations as your Shopify application scales.