The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Redis on AWS for Laravel
Nginx as a High-Performance Frontend for Laravel
When deploying Laravel applications on AWS, Nginx serves as the de facto standard for a high-performance web server and reverse proxy. Its event-driven, asynchronous architecture makes it exceptionally efficient at handling concurrent connections, serving static assets, and buffering requests to your application server. Proper tuning is crucial for maximizing throughput and minimizing latency.
Nginx Configuration for Laravel
The core of our Nginx configuration will reside in the server block. We’ll focus on optimizing worker processes, connection handling, caching, and request buffering.
Worker Processes and Connections
The worker_processes directive should ideally be set to the number of CPU cores available on your EC2 instance. The worker_connections directive defines 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 application’s needs and system limits.
Tuning for Static Assets
Laravel applications often serve static assets (CSS, JS, images). Nginx excels at this. We’ll configure caching headers and disable access logging for these files to reduce I/O and improve performance.
Request Buffering and Timeouts
For applications that might have longer-running requests or large uploads, tuning buffering directives is essential. This prevents Nginx from consuming excessive memory while waiting for the upstream application server.
Example Nginx Configuration Snippet
Here’s a production-ready snippet for your Nginx configuration, typically found in /etc/nginx/nginx.conf or a site-specific configuration file in /etc/nginx/sites-available/.
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto; # Set to number of CPU cores, or 'auto'
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on load and system limits
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hide Nginx version for security
# Gzip compression
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 upstream requests
client_body_buffer_size 128k;
client_max_body_size 100m; # Adjust for file uploads
client_header_buffer_size 1k;
large_client_header_buffers 4 32k;
# Proxy settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Static file caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)$ {
expires 365d;
add_header Cache-Control "public, no-transform";
access_log off; # Disable access logging for static files
log_not_found off;
}
# Laravel specific configuration (assuming PHP-FPM or Gunicorn)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Example for PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Ensure this path matches your PHP-FPM socket or address
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_read_timeout 300; # Increase timeout for long-running scripts
}
# Example for Gunicorn (if using a separate socket/port)
# location / {
# proxy_pass http://unix:/path/to/your/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;
# }
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Gunicorn/PHP-FPM Tuning for Laravel
The application server (Gunicorn for Python/WSGI, or PHP-FPM for PHP) is where your Laravel code actually executes. Optimizing its configuration is critical for handling the requests proxied by Nginx.
Gunicorn Configuration (Python/WSGI)
Gunicorn is a popular WSGI HTTP Server for Python. Its configuration revolves around worker processes and their types. For I/O-bound applications, the gevent or event worker classes are highly recommended. The number of workers is typically calculated as (2 * Number of CPU Cores) + 1.
Example Gunicorn Command Line
You’ll typically run Gunicorn via a systemd service or a process manager like Supervisor. Here’s an example command line:
# Example systemd service file (/etc/systemd/system/gunicorn.service) # ... other service configurations ... [Service] User=your_app_user Group=your_app_group WorkingDirectory=/var/www/your-laravel-app ExecStart=/usr/bin/gunicorn --workers 3 --worker-class gevent --bind unix:/var/run/gunicorn.sock --timeout 300 your_project.wsgi:application # Adjust --workers based on your CPU cores. # --worker-class gevent or event for I/O bound tasks. # --bind to a Unix socket is generally faster than TCP/IP for local communication. # --timeout in seconds.
PHP-FPM Configuration (PHP)
PHP-FPM (FastCGI Process Manager) is the standard for running PHP applications. Its tuning involves managing process pools, child processes, and request handling. The pm (process manager) setting is key: dynamic, static, or ondemand. For production, dynamic or static are usually preferred.
Example PHP-FPM Pool Configuration
This configuration is typically found in /etc/php/[version]/fpm/pool.d/www.conf.
; /etc/php/8.1/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock ; Or a TCP/IP address like 127.0.0.1:9000 ; Process Manager settings pm = dynamic pm.max_children = 50 ; Max number of children serving requests pm.start_servers = 5 ; Number of children created at startup pm.min_spare_servers = 2 ; Min number of idle respawners pm.max_spare_servers = 10 ; Max number of idle respawners pm.max_requests = 500 ; Max requests a child process will serve before respawning ; Adjust pm.max_children based on your server's RAM and expected load. ; A common starting point for pm.max_children is (Total RAM - OS/Other Processes RAM) / Average PHP Process Size. ; pm.max_requests helps prevent memory leaks in long-running processes.
Redis for Caching and Session Management
Redis is an invaluable tool for Laravel applications, primarily for caching and session management. Its in-memory nature provides extremely low latency for read/write operations, significantly offloading your database and improving response times.
AWS ElastiCache for Redis
For production environments on AWS, using Amazon ElastiCache for Redis is highly recommended. It handles the operational overhead of setup, patching, and scaling. Ensure your ElastiCache cluster is in the same VPC and subnet group as your EC2 instances, and configure security groups to allow access from your web servers.
Laravel Configuration for Redis
You’ll configure Redis in your Laravel application’s config/database.php and config/cache.php files. For sessions, you’ll modify config/session.php.
Example Laravel Redis Configuration
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'), // 'phpredis' or 'predis'
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
'cache' => [
'url' => env('REDIS_CACHE_URL'),
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_CACHE_DB', 1), // Use a different DB for cache
],
],
// config/cache.php
'default' => env('CACHE_DRIVER', 'file'),
'stores' => [
// ... other stores ...
'redis' => [
'driver' => 'redis',
'connection' => 'cache', // Points to the 'cache' Redis connection defined in config/database.php
],
// ... other stores ...
],
// config/session.php
'driver' => env('SESSION_DRIVER', 'file'),
// ... other session configurations ...
'redis' => [
'driver' => 'redis',
'connection' => 'default', // Points to the 'default' Redis connection
'table' => 'sessions',
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => false,
],
In your .env file, you would set these variables, pointing to your ElastiCache endpoint:
# .env file REDIS_HOST=your-elasticache-redis-endpoint.xxxxxx.cache.amazonaws.com REDIS_PASSWORD=your-redis-password # If using Redis AUTH REDIS_PORT=6379 REDIS_DB=0 REDIS_CACHE_DB=1 # Separate DB for cache
Monitoring and Iterative Tuning
Performance tuning is not a one-time event. Continuous monitoring is essential. Utilize AWS CloudWatch metrics for EC2 (CPU Utilization, Network In/Out), ElastiCache (Cache Hits/Misses, Evictions, CPU Usage), and Nginx/PHP-FPM logs. Tools like New Relic, Datadog, or Laravel’s own Telescope can provide deeper application-level insights.
Start with conservative settings and gradually increase them while observing performance metrics. Pay close attention to:
- CPU Utilization on EC2 instances.
- Memory usage (especially for PHP-FPM and Nginx worker processes).
- Redis Cache Hit Ratio and Evictions.
- Nginx
5xxerrors (indicating upstream issues). - Application response times.
By systematically tuning Nginx, your application server (Gunicorn/PHP-FPM), and Redis, you can build a robust, scalable, and high-performance infrastructure for your Laravel applications on AWS.