The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on Linode for Shopify
Nginx as a High-Performance Frontend for Shopify Applications
When deploying a Shopify application backend, particularly one built with Python (e.g., using Django or Flask) or PHP, Nginx serves as the de facto standard for a robust and performant frontend. Its event-driven architecture excels at handling a high volume of concurrent connections, making it ideal for serving static assets, proxying requests to application servers, and implementing crucial security and caching layers. This section focuses on tuning Nginx specifically for this role on a Linode VPS.
Core Nginx Configuration Tuning
The primary configuration file, typically located at /etc/nginx/nginx.conf, contains global settings that significantly impact performance. We’ll focus on the events and http blocks.
Optimizing the events Block
The events block controls how Nginx handles connections. The key directives here are worker_connections and multi_accept.
worker_connections defines the maximum number of simultaneous connections that each worker process can handle. This should be set in conjunction with the system’s file descriptor limit. A common starting point is 1024, but for high-traffic sites, this can be increased significantly. The theoretical maximum is limited by the OS’s file descriptor limit (ulimit -n).
multi_accept on; allows a worker process to accept as many new connections as possible in a single go, rather than accepting them one by one. This can improve performance under heavy load.
Here’s a sample tuning for the events block:
events {
worker_connections 4096; # Adjust based on system resources and expected load
multi_accept on;
use epoll; # For Linux, epoll is generally the most performant event notification mechanism
}
Tuning the http Block
The http block contains settings that apply to all virtual hosts. Key directives for performance include keepalive_timeout, sendfile, tcp_nopush, tcp_nodelay, and gzip.
keepalive_timeout: Controls how long an idle HTTP connection will remain open. A shorter timeout reduces server resource usage but might increase overhead for clients making frequent requests. A value between 65 and 75 seconds is a good balance.
sendfile on;: Enables the sendfile() system call, which allows Nginx to transfer files directly from the kernel’s page cache to the socket, bypassing user space. This significantly reduces CPU usage and memory overhead for serving static files.
tcp_nopush on;: Instructs Nginx to try and send HTTP response headers in one packet, along with any preceding data, if possible. This can reduce the number of packets sent over the network.
tcp_nodelay on;: Disables the Nagle algorithm. This is generally beneficial for latency-sensitive applications, ensuring that packets are sent as soon as they are available, rather than waiting to be buffered.
gzip on;: Enables Gzip compression for responses. This is crucial for reducing bandwidth usage and improving load times for text-based assets like HTML, CSS, and JavaScript. Further tuning of gzip_types, gzip_min_length, and gzip_comp_level is recommended.
Here’s an example of an optimized http block:
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 75;
types_hash_max_size 2048; # Increase if you have a very large number of MIME types
# Gzip Compression Settings
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Compression level (1-9)
gzip_min_length 256; # Minimum response length to compress
gzip_buffers 16 8k; # Number and size of buffers
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Optional: Enable brotli compression if compiled with it
# brotli on;
# brotli_comp_level 6;
# brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Include server blocks for your specific applications
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Proxying to Gunicorn (Python) or PHP-FPM
Nginx’s primary role for dynamic content is to act as a reverse proxy. The configuration within your site-specific Nginx configuration file (e.g., /etc/nginx/sites-available/your_shopify_app) is critical.
Nginx with Gunicorn (WSGI)
When using Gunicorn to serve a Python web application, Nginx typically communicates with Gunicorn via a Unix socket or a TCP port. Unix sockets are generally preferred for performance and security when Nginx and Gunicorn are on the same machine.
Key directives include proxy_pass, proxy_set_header, and proxy_read_timeout.
proxy_pass: Specifies the address of the upstream server (Gunicorn). If using a Unix socket, it would look like unix:/path/to/your/app.sock. If using TCP, it would be http://127.0.0.1:8000.
proxy_set_header: Essential for passing client information to the backend application. Directives like Host, X-Real-IP, and X-Forwarded-For are vital for the application to correctly identify the original client.
proxy_read_timeout: Sets the timeout for reading a response from the upstream server. For long-running requests, this needs to be sufficiently high to prevent premature timeouts.
Example Nginx configuration for Gunicorn:
server {
listen 80;
server_name your_domain.com www.your_domain.com;
# Serve static files directly
location /static/ {
alias /path/to/your/app/static/;
expires 30d;
add_header Cache-Control "public";
}
# Proxy dynamic requests to Gunicorn
location / {
proxy_pass http://unix:/run/gunicorn.sock; # Or http://127.0.0.1:8000;
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_connect_timeout 75s;
proxy_read_timeout 300s; # Adjust for long-running tasks
proxy_send_timeout 75s;
}
# Optional: Handle specific API endpoints with longer timeouts
location /api/ {
proxy_pass http://unix:/run/gunicorn.sock;
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 600s; # Even longer for specific API calls
}
# Access and error logs
access_log /var/log/nginx/your_app_access.log;
error_log /var/log/nginx/your_app_error.log;
}
Nginx with PHP-FPM
For PHP applications, Nginx acts as a reverse proxy to PHP-FPM, the FastCGI Process Manager. Communication is typically via a Unix socket or a TCP port.
The fastcgi_pass directive is used instead of proxy_pass. Other important directives include fastcgi_param, fastcgi_read_timeout, and fastcgi_buffers.
fastcgi_pass: Specifies the address of the PHP-FPM pool. E.g., unix:/var/run/php/php7.4-fpm.sock or 127.0.0.1:9000.
fastcgi_param: Sets FastCGI parameters. Crucially, SCRIPT_FILENAME must be set correctly for PHP to find the requested script.
fastcgi_read_timeout: Similar to proxy_read_timeout, this sets the timeout for reading a response from PHP-FPM.
fastcgi_buffers and fastcgi_buffer_size: Control the buffering of FastCGI responses. Increasing these can help with large output from PHP scripts.
Example Nginx configuration for PHP-FPM:
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_app/public; # Adjust to your web root
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf; # Common FastCGI settings
# Or define parameters explicitly:
# fastcgi_split_path_info ^(.+\.php)(/.+)$;
# fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# include fastcgi_params;
# Use the correct PHP-FPM socket or address
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Example for PHP 8.1
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $fastcgi_path_translated;
fastcgi_read_timeout 300s; # Adjust for long-running PHP scripts
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
}
# Deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
location ~ /\.ht {
deny all;
}
# Access and error logs
access_log /var/log/nginx/your_app_access.log;
error_log /var/log/nginx/your_app_error.log;
}
System-Level Tuning for Nginx
Beyond Nginx’s configuration, the underlying operating system needs to be tuned to support high concurrency.
File Descriptor Limits
Each connection and open file consumes a file descriptor. Ensure your system’s limits are set appropriately. This is typically managed via /etc/security/limits.conf and systemd service files.
Edit /etc/security/limits.conf:
# Increase open file limits for the nginx user nginx soft nofile 65536 nginx hard nofile 65536
For systemd services (like Nginx or Gunicorn), you might need to configure limits within the service unit file:
[Service] LimitNOFILE=65536 LimitNOFILESoft=65536
After modifying limits.conf, you’ll need to log out and log back in for the changes to take effect for your user. For systemd services, reload the daemon and restart the service:
sudo systemctl daemon-reload sudo systemctl restart nginx sudo systemctl restart gunicorn # or php-fpm
Network Stack Tuning (sysctl)
Optimizing kernel network parameters can improve TCP performance and connection handling.
Edit /etc/sysctl.conf and add or modify the following parameters:
# Increase the maximum number of open files fs.file-max = 2097152 # Increase the maximum number of file descriptors that can be opened by a process # This is a system-wide limit, often set higher than individual user limits kernel.pid_max = 65536 # Network tuning net.core.somaxconn = 4096 # Maximum number of connections queued net.core.netdev_max_backlog = 2000 # Max number of packets queued on the input side net.ipv4.tcp_max_syn_backlog = 2048 # Max number of outstanding SYN net.ipv4.tcp_synack_retries = 2 # Number of SYN-ACK retries net.ipv4.tcp_tw_reuse = 1 # Allow reuse of TIME-WAIT sockets net.ipv4.tcp_fin_timeout = 30 # Timeout for closing sockets net.ipv4.ip_local_port_range = 1024 65535 # Range of ephemeral ports net.ipv4.tcp_timestamps = 0 # Disable TCP timestamps (can save a little bandwidth, but might affect some diagnostics) net.ipv4.tcp_syncookies = 1 # Enable TCP syncookies to protect against SYN floods
Apply the changes:
sudo sysctl -p
Gunicorn Performance Tuning for Python Applications
Gunicorn (Green Unicorn) is a Python WSGI HTTP Server. Its performance is heavily influenced by its worker configuration and how it interacts with the OS.
Worker Types and Counts
Gunicorn supports several worker types:
- Sync Workers (
sync): The default. Each worker is a single process that handles one request at a time. This is simple but can be a bottleneck under high concurrency. - Asynchronous Workers (
eventlet,gevent): These workers use non-blocking I/O and can handle multiple requests concurrently within a single process using green threads. This is generally the preferred choice for I/O-bound applications. - Threaded Workers (
gthread): Uses threads within a single process. Less common for web applications due to Python’s Global Interpreter Lock (GIL), but can be useful for specific CPU-bound tasks if carefully managed.
The number of workers is crucial. A common recommendation is (2 * number_of_cores) + 1. However, for I/O-bound applications using async workers, you might need more workers to keep all CPU cores busy while waiting for I/O. For sync workers, this formula is more appropriate.
Example Gunicorn command-line configuration:
gunicorn --workers 4 \
--worker-class gevent \
--bind unix:/run/gunicorn.sock \
--timeout 300 \
--graceful-timeout 300 \
--log-level info \
your_app.wsgi:application
Explanation:
--workers 4: Sets the number of worker processes. Adjust based on your Linode instance’s CPU cores.--worker-class gevent: Uses gevent for asynchronous handling.--bind unix:/run/gunicorn.sock: Binds to a Unix socket for Nginx to connect to.--timeout 300: Sets the worker timeout to 300 seconds (5 minutes).--graceful-timeout 300: Time to wait for existing requests to finish during a graceful restart.your_app.wsgi:application: The entry point for your WSGI application.
Gunicorn Configuration File
For more complex configurations, using a Gunicorn configuration file (e.g., gunicorn_config.py) is recommended.
# gunicorn_config.py import multiprocessing # Number of worker processes workers = multiprocessing.cpu_count() * 2 + 1 # Worker class (gevent, eventlet, sync, gthread) worker_class = 'gevent' # Or 'sync' if not using async libraries # Bind to a Unix socket or TCP port # bind = "unix:/run/gunicorn.sock" bind = "127.0.0.1:8000" # If binding to TCP, Nginx will proxy to this # Worker timeout (seconds) timeout = 300 # Graceful timeout (seconds) graceful_timeout = 300 # Logging log_level = 'info' accesslog = '/var/log/gunicorn/access.log' errorlog = '/var/log/gunicorn/error.log' # Other settings # max_requests = 1000 # Restart workers after this many requests # preload_app = True # Preload the application to speed up worker startup
To run Gunicorn with a config file:
gunicorn -c /path/to/your/gunicorn_config.py your_app.wsgi:application
PHP-FPM Tuning for PHP Applications
PHP-FPM is the FastCGI Process Manager for PHP. Its performance is critical for PHP applications. The configuration is managed in php-fpm.conf and pool configuration files (e.g., www.conf).
Process Manager Settings
The pm (Process Manager) setting controls how PHP-FPM manages worker processes. The common options are:
- static: A fixed number of child processes are always kept alive. Good for predictable loads, but can waste resources if idle.
- dynamic: The number of child processes varies based on load. It starts with a minimum number and spawns more up to a maximum.
- ondemand: Processes are spawned only when a request arrives and are killed after a period of inactivity. This saves resources but can introduce latency on the first request.
For most production environments, dynamic offers a good balance. For very high-traffic sites with predictable loads, static might offer slightly better performance by eliminating process spawning overhead.
Key directives within the pool configuration (e.g., /etc/php/8.1/fpm/pool.d/www.conf):
pm.max_children: The maximum number of child processes to be created whenpmis set todynamicorstatic.pm.start_servers: The number of child processes to start when the pool starts (fordynamic).pm.min_spare_servers: The minimum number of idle (spare) server processes to maintain (fordynamic).pm.max_spare_servers: The maximum number of idle (spare) server processes to maintain (fordynamic).pm.max_requests: The number of requests each child process should execute before respawning. Setting this helps prevent memory leaks.
Example www.conf tuning:
[www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock # Or 127.0.0.1:9000 ; Choose your process manager pm = dynamic ; Settings for dynamic PM pm.max_children = 100 ; Adjust based on RAM and expected concurrency pm.start_servers = 10 ; Initial number of workers pm.min_spare_servers = 5 ; Minimum idle workers pm.max_spare_servers = 20 ; Maximum idle workers pm.max_requests = 500 ; Respawn worker after this many requests ; Settings for static PM (if chosen) ; pm = static ; pm.max_children = 100 ; Settings for ondemand PM (if chosen) ; pm = ondemand ; pm.process_idle_timeout = 10s ; pm.max_children = 100 ; Other important settings request_terminate_timeout = 300 ; Timeout for script execution (seconds) ; php_admin_value[memory_limit] = 256M ; Example: Set memory limit per script ; php_admin_value[upload_max_filesize] = 64M ; php_admin_value[post_max_size] = 64M
After modifying PHP-FPM configuration, reload the service:
sudo systemctl reload php8.1-fpm
MongoDB Performance Tuning on Linode
MongoDB’s performance is heavily dependent on hardware, configuration, and query patterns. On Linode, optimizing disk I/O, memory usage, and network is key.
Storage Engine Choice
MongoDB primarily uses the WiredTiger storage engine. It’s generally the best choice for most workloads due to its document-level concurrency, compression, and caching.
Ensure your Linode instance has sufficient RAM. MongoDB heavily relies on RAM for its cache. A common recommendation is to have enough RAM to hold your working set (frequently accessed data and indexes).
MongoDB Configuration File (mongod.conf)
The main configuration file is typically located at /etc/mongod.conf.
Key Configuration Parameters
storage.wiredTiger.engineConfig.cacheSizeGB: This is arguably the most critical parameter. It defines the maximum amount of RAM WiredTiger can use for its internal cache. A common starting point is to allocate 50% of your system’s RAM to this cache, ensuring you leave enough for the OS and other processes.
operationProfiling.mode and operationProfiling.slowOpThresholdMs: Enable slow query logging to identify performance bottlenecks. Set mode to all or off, and slowOpThresholdMs to a value like 100ms or 200ms.
net.bindIp: If MongoDB is only accessed from the same Linode instance (via Nginx/Gunicorn/PHP-FPM), bind it to 127.0.0.1 for security. If it needs to be accessed remotely, ensure proper firewall rules are in place.
systemLog.path and systemLog.logAppend: Configure logging paths and ensure logs are appended.
Example mongod.conf snippet (adjusting for a 16GB RAM Linode instance):
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
logRotate: reopen
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 7.5 # Allocate ~50% of 16GB RAM, leaving room for OS/other processes
collectionConfig:
blockSize: 4KB # Default, adjust if your data patterns benefit from larger/smaller blocks
indexConfig:
prefixCompression: true # Enable prefix compression for indexes
net:
port: 27017
bindIp: 127.0.0.1 # Or 0.0.0.0 if remote access is required (with firewall)
operationProfiling:
mode: slowOp # Log slow operations
slowOpThresholdMs: 100 # Log operations slower than 100ms
# Sharding settings (if applicable)
# sharding:
# clusterRole: configsvr
# # or shardsvr
# Replication settings (if applicable)
# replication:
# replSetName: rs0
After modifying mongod.conf, restart MongoDB:
sudo systemctl restart mongod
Indexing Strategy
Proper indexing is paramount for MongoDB performance. Analyze your application’s query patterns using the slow query logs and explain() output.
Use the MongoDB shell to analyze queries:
db.collection.find({ field1: "value1", field2: "value2" }).explain("executionStats")
This will show you if indexes are being used, the number of documents scanned, and the query execution time. Create compound indexes where appropriate for queries involving multiple fields.
Monitoring and Diagnostics
Regular monitoring is essential. Use tools like:
- Linode Cloud Manager: For CPU, RAM, Disk I/O, and Network usage.
- Nginx Status Module:
stub_statusto monitor active connections, requests, etc. - PHP-FPM Status Page: To monitor active processes, idle processes, and requests.
- MongoDB Tools:
mongostat,mongotop, and the MongoDB shell’sdb.serverStatus()anddb.stats(). - Application Performance Monitoring (APM) tools: e.g., New Relic, Datadog, Sentry for deeper application-level insights.
For Nginx, enable the stub_status module in your nginx.conf:
http {
# ... other http settings ...
server {
listen 80;
server_name status.your_domain.com; # Or a specific internal IP/port
location /nginx_status {
stub_status on;
allow 127.0.0.1; # Restrict access
deny all;
}
}
}
For PHP-FPM, enable the status page in your pool configuration (e.g., www.conf):
; Add this to your pool configuration pm.status_path = /fpm-status ping.path = /fpm-ping ping.response = pong
Then, in your Nginx site configuration, add a location block to serve it:
location ~ ^/(fpm-status|fpm-ping)$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Match your PHP-FPM socket
include fastcgi_params;
allow 127.0.0.1;
deny all;
}
Use mongostat and mongotop from the command line for real-time MongoDB insights:
# Real-time connection and operation stats mongostat --host 127.0.0.1:27017 --discover --noheaders --interval 5 # Real-time document lock analysis mongotop --host 127.0.0.1:27017 --discover --noheaders --interval 5
By systematically tuning Nginx, your application server (Gunicorn/PHP-FPM), and your database (MongoDB), you can build a highly performant and scalable infrastructure on Linode for your Shopify backend.