The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Redis on DigitalOcean for Ruby
Optimizing Nginx for High-Traffic Ruby Applications
When deploying Ruby applications on DigitalOcean, Nginx serves as the critical front-end, handling incoming HTTP requests, SSL termination, static file serving, and proxying to your application server. Proper Nginx tuning is paramount for achieving low latency and high throughput. This section focuses on key directives and strategies for production environments.
Nginx Configuration for Ruby Applications
The core of our Nginx configuration will reside within the http block, specifically in a server block that defines our application’s virtual host. We’ll focus on directives that impact performance and resource utilization.
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 detect the number of CPU cores and utilize them efficiently. The worker_connections directive sets the maximum number of simultaneous connections that each worker process can handle. A common starting point is 1024, but this can be increased based on your server’s RAM and expected load.
Keep-Alive and Buffers
keepalive_timeout controls how long an idle HTTP connection will remain open. A shorter timeout (e.g., 65 seconds) can free up resources faster, while a longer one can improve performance for clients making multiple requests. client_body_buffer_size and client_header_buffer_size are important for handling large requests. Setting them appropriately can prevent Nginx from writing to disk for temporary storage.
Gzip Compression
Enabling Gzip compression significantly reduces the size of responses sent to the client, leading to faster load times. Ensure you’re only compressing text-based assets.
Static File Serving Optimization
Nginx excels at serving static files. Directives like expires and add_header Cache-Control instruct browsers to cache static assets, reducing the load on your application server and Nginx itself for subsequent requests.
Proxying to Application Server (Gunicorn/FPM)
The proxy_pass directive is fundamental. For Gunicorn (Python WSGI), it will point to the Gunicorn socket or IP:Port. For PHP-FPM, it will point to the FPM socket. Crucially, set appropriate proxy headers to ensure your application receives accurate client information.
Example Nginx Configuration Snippet
Here’s a robust Nginx configuration snippet tailored for a Ruby application proxied to Gunicorn, running on a DigitalOcean droplet. Adapt paths and ports as necessary.
# /etc/nginx/sites-available/your_app.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on server RAM and expected load
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;
# SSL Settings
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';
# 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;
# Buffers
client_body_buffer_size 100K;
client_header_buffer_size 1K;
large_client_header_buffers 2 128K;
# Logging
access_log /var/log/nginx/your_app.access.log;
error_log /var/log/nginx/your_app.error.log;
# Static File Caching
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# Proxy to Gunicorn
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://unix:/path/to/your/app/shared/app.sock; # Or http://127.0.0.1:8000;
}
# Optional: Serve static files directly if not handled by app framework
# location /static/ {
# alias /path/to/your/app/static/;
# expires 30d;
# add_header Cache-Control "public";
# }
}
Tuning Gunicorn for Production
Gunicorn (Green Unicorn) is a popular WSGI HTTP Server for Python. When deploying Ruby applications, you might be using a Ruby-specific server like Puma or Unicorn, but the principles of tuning worker processes and threads are similar. For this example, we’ll assume a Python context for Gunicorn, but the concepts translate. If you’re using Ruby’s Puma, adjust the worker/thread counts accordingly.
Worker Processes and Threads
Gunicorn’s performance is heavily influenced by its worker processes and threads. The --workers flag determines the number of worker processes. A common recommendation is (2 * number_of_cpu_cores) + 1. For I/O-bound applications, you might also leverage threads using the --threads flag. However, be mindful that threads share the same process memory, and excessive threads can lead to context-switching overhead.
Worker Class
Gunicorn supports different worker classes. The default is sync, which is a simple, single-threaded worker. For I/O-bound applications, gevent or eventlet (asynchronous workers) can significantly improve concurrency by using green threads. If your application is CPU-bound, the sync worker class with multiple processes might be more suitable.
Timeouts and Keep-Alive
--timeout specifies the number of seconds to wait for a worker to respond before considering it dead. Adjust this based on your application’s typical request processing time. --keep-alive controls the number of requests a worker can handle before it’s restarted, helping to prevent memory leaks.
Example Gunicorn Command Line
This command line demonstrates a production-ready Gunicorn setup. It uses the sync worker class, a reasonable number of workers, and a socket for Nginx to connect to.
# Example Gunicorn command (adjust paths and worker counts)
# Assuming your WSGI application is in 'wsgi.py' and named 'application'
# For Ruby, this would be your Rackup file and server (e.g., Puma)
gunicorn --workers 3 \
--threads 2 \
--worker-class sync \
--bind unix:/path/to/your/app/shared/app.sock \
--timeout 30 \
--keep-alive 1000 \
wsgi:application
Note for Ruby: If you are using Puma, your command might look like:
# Example Puma command (adjust paths and worker/thread counts) # Assuming your Rackup file is 'config.ru' bundle exec puma -w 3 -t 5 -b 'unix:/path/to/your/app/shared/app.sock' -e production
Redis Performance Tuning
Redis is an invaluable tool for caching, session management, and message queuing. Optimizing its configuration on DigitalOcean can drastically improve application responsiveness.
Memory Management
maxmemory is crucial. Set this to a value that leaves sufficient RAM for your operating system and other services. maxmemory-policy determines how Redis evicts keys when maxmemory is reached. allkeys-lru (Least Recently Used) is a common and effective policy for caching scenarios.
Persistence
For production, you’ll likely want some form of persistence. RDB (snapshotting) and AOF (Append Only File) are the two main options. RDB is generally faster for restores but can lose data between snapshots. AOF logs every write operation, offering better durability but potentially slower restores and larger file sizes. Tuning save directives for RDB and appendfsync for AOF is important.
Network and I/O
tcp-backlog can be increased to handle a higher number of incoming connections. tcp-keepalive helps detect and close stale connections.
Example Redis Configuration Snippet
This snippet provides a solid starting point for Redis configuration on a DigitalOcean droplet. Adjust memory limits and persistence settings based on your specific needs and droplet size.
# /etc/redis/redis.conf # General daemonize yes pidfile /var/run/redis/redis-server.pid port 6379 tcp-backlog 511 # Default is 511, can be increased if needed # Memory Management # Set maxmemory to a value that leaves ~25% of RAM for OS and other services # Example for a 4GB RAM droplet: maxmemory 3gb maxmemory-policy allkeys-lru # Or volatile-lru if using TTLs extensively # Persistence (Choose one or both, tune carefully) # RDB Snapshotting save 900 1 # Save if 1 key changed in 900 seconds save 300 10 # Save if 10 keys changed in 300 seconds save 60 10000 # Save if 10000 keys changed in 60 seconds dbfilename dump.rdb # AOF (Append Only File) - More durable, potentially slower restores appendonly yes appendfilename "appendonly.aof" appendfsync everysec # 'always' is safer but slower, 'no' is fastest but riskiest # Network tcp-keepalive 300 # Logging loglevel notice logfile /var/log/redis/redis-server.log
Monitoring and Iterative Tuning
Performance tuning is not a one-time event. Continuous monitoring is essential. Utilize tools like:
- Nginx:
nginx -s reloadfor applying config changes,stub_statusmodule for connection metrics, and access/error logs. - Gunicorn/Puma: Application logs, process monitoring tools (e.g.,
htop,ps), and potentially APM tools. - Redis:
redis-cli INFOcommand provides a wealth of metrics (memory usage, connected clients, commands processed, etc.). - System-wide:
top,htop,vmstat,iostatto monitor CPU, memory, and I/O.
Start with conservative settings and gradually increase them while observing performance metrics. Identify bottlenecks through monitoring and adjust configurations iteratively. For instance, if Nginx logs show frequent 502 Bad Gateway errors, investigate your application server (Gunicorn/Puma) and its connection to Nginx. If Redis commands are slow, check memory usage and eviction policies.