The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Elasticsearch on DigitalOcean for Shopify
Nginx as a High-Performance Frontend for PHP/Python Applications
When deploying PHP (via FPM) or Python (via Gunicorn) applications on DigitalOcean for a Shopify integration, Nginx serves as the de facto standard for a high-performance frontend. Its event-driven architecture excels at handling concurrent connections, serving static assets, and acting as a robust reverse proxy. The key to unlocking its full potential lies in meticulous configuration, particularly around worker processes, connection limits, and caching.
Nginx Worker Processes and Connections
The `worker_processes` directive dictates how many worker processes Nginx will spawn. Setting this to `auto` is generally recommended, allowing Nginx to dynamically adjust based on the number of CPU cores available. This ensures optimal CPU utilization without over-subscription. Complementing this is `worker_connections`, which defines the maximum number of simultaneous connections a single worker process can handle. The total maximum connections will be `worker_processes * worker_connections`.
A common pitfall is setting `worker_connections` too low, leading to connection exhaustion under load. Conversely, setting it excessively high can lead to resource contention. A good starting point for a DigitalOcean droplet with 2-4 vCPUs is 1024 or 2048. For larger instances, this can be scaled up. It’s crucial to also tune the operating system’s file descriptor limits to accommodate these connections.
Tuning `ulimit` for Nginx
Before Nginx can effectively utilize high connection counts, the underlying operating system must permit it. This is managed via `ulimit` settings, specifically the maximum number of open file descriptors. We need to increase this limit for the user running Nginx (typically `www-data` or `nginx`).
Setting System-Wide Limits
Edit the `/etc/security/limits.conf` file to set persistent limits. Add the following lines, replacing `nofile` with the desired limit (e.g., 65536):
* soft nofile 65536 * hard nofile 65536 root soft nofile 65536 root hard nofile 65536 www-data soft nofile 65536 www-data hard nofile 65536
Then, edit `/etc/sysctl.conf` to adjust kernel-level network parameters, particularly the maximum number of open files the kernel can allocate.
fs.file-max = 2097152 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30 net.ipv4.tcp_tw_recycle = 0 net.ipv4.ip_local_port_range = 1024 65535 net.core.somaxconn = 4096 net.ipv4.tcp_max_syn_backlog = 2048
Apply these changes immediately with:
sudo sysctl -p
After modifying `limits.conf`, you’ll need to restart the Nginx service or log out and back in for the changes to take effect for the Nginx user.
Nginx Configuration for PHP-FPM
When using PHP-FPM, Nginx acts as a reverse proxy, forwarding requests to the FPM process. The `fastcgi_pass` directive is critical here. It can point to a Unix socket (preferred for performance on a single server) or a TCP port.
Optimizing `fastcgi_params`
Ensure your `fastcgi_params` (or a custom equivalent) includes essential variables. The following is a robust set:
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; fastcgi_param HTTPS $https if_not_empty; fastcgi_param REDIRECT_STATUS 200; # For clean URLs fastcgi_param HTTP_HOST $http_host; fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for; fastcgi_param HTTP_CONNECTION $connection_upgrade;
Example Nginx Site Configuration (PHP-FPM)
This configuration prioritizes caching static assets and efficiently proxies dynamic requests to PHP-FPM. Adjust `client_max_body_size` based on expected upload sizes.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjusted based on ulimit
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Buffering for dynamic content
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# SSL Configuration (if applicable)
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_prefer_server_ciphers on;
# 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;
# Static File Caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
# Proxy to PHP-FPM
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
# Use Unix socket for performance if FPM is on the same server
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust PHP version as needed
# Or use TCP if FPM is on a different host/port
# fastcgi_pass 127.0.0.1:9000;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# PHP-FPM error logging
location ~ \.php$ {
include snippets/fastcgi-php.conf; # Or include your custom fastcgi_params
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust PHP version as needed
}
}
Nginx Configuration for Gunicorn (Python)
For Python applications, Gunicorn is a popular WSGI HTTP Server. Nginx will proxy requests to Gunicorn, typically via a Unix socket or a local TCP port. The configuration is similar to PHP-FPM but uses `proxy_pass` instead of `fastcgi_pass`.
Example Nginx Site Configuration (Gunicorn)
This configuration assumes Gunicorn is listening on a Unix socket. Adjust `proxy_read_timeout` and `proxy_connect_timeout` based on your application’s typical response times.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Buffering for dynamic content
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Gunicorn Proxy Settings
location / {
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 to Gunicorn Unix Socket
proxy_pass http://unix:/path/to/your/gunicorn.sock; # Adjust path
# Or proxy to Gunicorn TCP socket
# proxy_pass http://127.0.0.1:8000; # Adjust port
proxy_read_timeout 300s; # Increase for long-running requests
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
}
# Static File Caching (if Gunicorn serves static files, otherwise serve via Nginx directly)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
# If static files are served by Gunicorn, remove this block and let proxy_pass handle it.
# If Nginx serves static files, ensure 'root' directive is set correctly.
}
}
Gunicorn Configuration Best Practices
Gunicorn’s performance is heavily influenced by its worker count and type. For most applications, the `sync` worker class is stable, but `gevent` or `event` can offer better concurrency if your application is I/O bound.
Worker Count and Type
A common recommendation is `(2 * number_of_cores) + 1`. However, this can be adjusted based on your application’s memory footprint and I/O patterns. For I/O-bound tasks, increasing workers can be beneficial. For CPU-bound tasks, fewer workers might be optimal to avoid context switching overhead.
Example Gunicorn Command Line
This command starts Gunicorn with 4 worker processes (sync type), listening on a Unix socket. Adjust `workers` and `worker_class` as needed.
gunicorn --workers 4 --worker-class sync --bind unix:/path/to/your/gunicorn.sock your_project.wsgi:application
For a TCP socket:
gunicorn --workers 4 --worker-class sync --bind 127.0.0.1:8000 your_project.wsgi:application
Elasticsearch Performance Tuning on DigitalOcean
Elasticsearch, often used for product search and logging within a Shopify integration, can become a performance bottleneck if not properly configured. DigitalOcean droplets, especially those with limited RAM, require careful JVM heap sizing and OS-level tuning.
JVM Heap Size (`jvm.options`)
The most critical setting is the JVM heap size. It should be set to no more than 50% of the total system RAM, and never exceed 30-32GB (due to compressed ordinary object pointers, or “compressed oops”).
Edit the `jvm.options` file (typically located at `/etc/elasticsearch/jvm.options` or within the Elasticsearch installation directory). Find the lines starting with `-Xms` (initial heap size) and `-Xmx` (maximum heap size) and set them appropriately. For a 4GB RAM droplet, a good starting point is 2GB.
-Xms2g -Xmx2g
After changing `jvm.options`, restart Elasticsearch:
sudo systemctl restart elasticsearch
OS-Level Tuning for Elasticsearch
Elasticsearch benefits from increased virtual memory limits and disabled swapping.
Disable Swap
Swapping can severely degrade Elasticsearch performance. Disable it and remove swap entries from `/etc/fstab`.
sudo swapoff -a # Then edit /etc/fstab and comment out or remove swap lines
Increase `vm.max_map_count`
Elasticsearch requires a high number of memory map areas. Edit `/etc/sysctl.conf` or create a file in `/etc/sysctl.d/`:
vm.max_map_count=262144
Apply the change:
sudo sysctl -p
Elasticsearch Indexing and Sharding Strategy
For Shopify integrations, product data is a prime candidate for Elasticsearch. A common mistake is creating overly large indices or an excessive number of shards. For product catalogs, consider:
- Shards: Start with a small number of primary shards (e.g., 1-3) per index. You can add more later if needed, but reducing them is difficult. The number of shards should ideally be related to the number of data nodes, not the total document count.
- Replicas: Use replicas for high availability and read performance. A common setup is 1 replica for production.
- Index Lifecycle Management (ILM): For time-series data (like logs), use ILM to automatically manage indices (rollover, shrink, delete).
- Mapping: Define explicit mappings for your product data to ensure correct data types and avoid dynamic mapping overhead.
Monitoring and Diagnostics
Regular monitoring is crucial. Use tools like:
- Nginx: `stub_status` module for connection metrics, `access.log` analysis (e.g., with `goaccess`), and `error.log`.
- Gunicorn/PHP-FPM: Application logs, process monitoring (e.g., `htop`, `ps`), and potentially APM tools.
- Elasticsearch: Elasticsearch’s own monitoring APIs (`_cat` APIs, `_nodes/stats`, `_indices/stats`), and external tools like Metricbeat or Prometheus/Grafana.
For Nginx, enable `stub_status` in your `http` block:
http {
# ... other http settings ...
server {
listen 80;
server_name your_domain.com;
location /nginx_status {
stub_status;
allow 127.0.0.1; # Restrict access
deny all;
}
# ... rest of your server config ...
}
}
This provides a quick overview of active connections, accepted connections, etc., accessible at `/nginx_status`.