The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Redis on DigitalOcean for Laravel
Nginx as a High-Performance Frontend for Laravel
When deploying Laravel applications, Nginx serves as an indispensable frontend, efficiently handling static assets, SSL termination, and proxying requests to your application server. Proper tuning is critical for maximizing throughput and minimizing latency. We’ll focus on key directives that impact performance.
Core Nginx Configuration Tuning
The primary configuration file, typically located at /etc/nginx/nginx.conf, contains global settings. For production environments, consider the following adjustments:
- worker_processes: Set this to the number of CPU cores available on your server. This allows Nginx to utilize all available processing power for handling concurrent connections.
- worker_connections: This defines the maximum number of simultaneous connections that each worker process can handle. A common starting point is
1024or higher, depending on expected traffic. - multi_accept: Setting this to
onallows worker processes to accept as many new connections as possible at once, rather than one at a time. - keepalive_timeout: This directive specifies the time a persistent connection will remain open. A value between
60and120seconds is often a good balance between resource usage and client responsiveness. - sendfile: Set to
onto enable efficient file transfer from the kernel’s page cache, reducing data copying between user space and kernel space. - tcp_nopush: Set to
onto enable Nginx to send header information with the file data in one packet, reducing the number of packets sent. - tcp_nodelay: Set to
onto disable the Nagle algorithm, which can reduce latency for real-time applications by sending small packets immediately.
Here’s an example snippet for nginx.conf:
worker_processes auto;
worker_connections 4096;
multi_accept on;
events {
worker_connections 4096;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 120;
keepalive_requests 1000;
# Gzip compression for text-based assets
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;
# Caching for static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# ... other http configurations ...
}
Laravel Application Server Configuration (Gunicorn/PHP-FPM)
The choice between Gunicorn (for Python-based frameworks like Flask/Django, but often used with PHP via tools like php-fpm) and PHP-FPM (for PHP applications like Laravel) dictates the specific tuning parameters. We’ll cover PHP-FPM as it’s the standard for Laravel.
PHP-FPM Tuning
PHP-FPM’s configuration, typically found in /etc/php/[version]/fpm/php-fpm.conf and /etc/php/[version]/fpm/pool.d/www.conf, is crucial for managing PHP processes. The pm (process manager) setting is key.
- pm: Options include
static,dynamic, andondemand.static: Pre-forks a fixed number of child processes. Good for predictable loads, but can be wasteful if idle.dynamic: Starts with a few processes and spawns more as needed, up to a `pm.max_children` limit. It also kills idle processes. This is generally the recommended setting for most web applications.ondemand: Spawns processes only when a request comes in and kills them after a timeout. Can save resources but might introduce slight latency on the first request.
- pm.max_children: The maximum number of child processes that can be alive at the same time. This should be carefully tuned based on your server’s RAM. A common formula is
(Total RAM - Web Server RAM) / Average Process Size. - pm.start_servers: The number of child processes to start when PHP-FPM starts.
- pm.min_spare_servers: The minimum number of idle (spare) processes that should be kept active.
- pm.max_spare_servers: The maximum number of idle (spare) processes that should be kept active.
- request_terminate_timeout: The maximum time a script is allowed to run before being terminated. Crucial for preventing runaway scripts.
- pm.process_idle_timeout: The number of seconds after which an idle process will be killed. (Only applicable for
dynamicandondemand).
Example www.conf for a DigitalOcean droplet with 4GB RAM (adjust `pm.max_children` based on your actual RAM and typical PHP process memory footprint):
; /etc/php/8.1/fpm/pool.d/www.conf (example for PHP 8.1) ; Adjust based on your server's RAM and typical PHP process memory usage. ; For 4GB RAM, a starting point for pm.max_children might be around 100-150. ; Monitor memory usage closely. [www] user = www-data group = www-data listen = /run/php/php8.1-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 150 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.process_idle_timeout = 10s ; Maximum execution time of scripts. max_execution_time = 60 ; Maximum amount of time each script may spend decoding input data. max_input_time = 60 ; Maximum amount of memory a script may consume (e.g. 256MB). memory_limit = 256M ; Request terminate timeout. request_terminate_timeout = 120 ; Set to 'no' for production to avoid potential issues with error reporting. ; display_errors = off ; Log errors to stderr. ; log_level = notice ; error_log = /var/log/php/php8.1-fpm.log
And the corresponding Nginx configuration to proxy requests to PHP-FPM:
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_laravel_app/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Make sure this matches your PHP-FPM pool configuration
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to .env files
location ~ /\.env {
deny all;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
}
Redis for Caching and Session Management
Redis is a powerful in-memory data structure store, commonly used for caching, session storage, and message brokering in Laravel applications. Optimizing Redis performance involves both server-side configuration and client-side usage patterns.
Redis Server Configuration (`redis.conf`)
The main Redis configuration file is typically located at /etc/redis/redis.conf. Key parameters for performance include:
maxmemory: Set a limit on the amount of memory Redis can use. This prevents Redis from consuming all available RAM and causing system instability. Choose a value that leaves sufficient memory for the OS and other services.maxmemory-policy: Defines how Redis evicts keys whenmaxmemoryis reached. For caching,allkeys-lru(Least Recently Used) orvolatile-lru(for keys with an expire set) are common choices.savedirectives: These control RDB snapshotting. For high-traffic, volatile caches, you might disable or significantly reduce the frequency of RDB saves to avoid I/O overhead. If persistence is critical, tune these carefully.appendonly: If persistence is required, enabling AOF (Append Only File) can provide better durability than RDB. Tuneappendfsync(e.g.,everysec) to balance durability and performance.tcp-backlog: The maximum number of pending connections. Increasing this can help during sudden traffic spikes.
Example snippet for redis.conf on a DigitalOcean droplet:
# /etc/redis/redis.conf # Set a memory limit (e.g., 2GB for a server with 4GB RAM, leaving room for OS and app) maxmemory 2gb # Eviction policy for when maxmemory is reached. LRU is good for caches. maxmemory-policy allkeys-lru # Disable RDB snapshots if Redis is purely for volatile cache and AOF is enabled # save "" # Enable AOF persistence for better durability if needed # appendonly yes # appendfsync everysec ; Balance between durability and performance # Increase TCP backlog for handling more concurrent connections tcp-backlog 511 # Bind to a specific IP or localhost if only accessed locally # bind 127.0.0.1 ::1 # Or bind to a private IP if accessed from other droplets in the same VPC # bind 10.10.0.5 # Require a password for security # requirepass your_strong_redis_password
After modifying redis.conf, restart the Redis service:
sudo systemctl restart redis-server
Laravel Redis Configuration (`.env`)
Ensure your Laravel application’s .env file is correctly configured to connect to your Redis instance. For production, it’s highly recommended to use a password and bind Redis to a private IP or localhost.
# .env file in your Laravel project REDIS_HOST=127.0.0.1 REDIS_PASSWORD=your_strong_redis_password REDIS_PORT=6379 # If Redis is on a different server or accessed via private IP # REDIS_HOST=10.10.0.5 # REDIS_PASSWORD=your_strong_redis_password # REDIS_PORT=6379
Monitoring and Iterative Tuning
Performance tuning is an ongoing process. Regularly monitor your system’s resource utilization and application performance metrics. Key tools and metrics include:
- Nginx: Use tools like
nginx-statusmodule or Prometheus exporters to monitor active connections, requests per second, and error rates. - PHP-FPM: Monitor the number of active, idle, and busy processes via the FPM status page (requires enabling
pm.status_pathinwww.confand configuring Nginx to access it). - Redis: Use
redis-cli INFOto check memory usage, connected clients, cache hit rates (if applicable), and command latency. - System Resources: Monitor CPU usage, RAM usage, disk I/O, and network traffic using tools like
htop,vmstat,iostat, and DigitalOcean’s monitoring dashboard. - Application Performance Monitoring (APM): Tools like New Relic, Datadog, or Sentry can provide deep insights into application bottlenecks, slow database queries, and external service calls.
Start with conservative settings and gradually increase limits (e.g., pm.max_children, worker_connections) while observing resource usage. If you encounter out-of-memory errors, scale back the limits. If your application is consistently CPU-bound, consider optimizing your code or scaling up your droplet.