The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on AWS for Laravel
Nginx Tuning for Laravel Applications
Optimizing Nginx is crucial for serving Laravel applications efficiently, especially under load. We’ll focus on key directives that impact performance, concurrency, and resource utilization on AWS.
Worker Processes and Connections
The worker_processes directive controls 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 effectively. 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 should be tuned based on your application’s concurrency needs and server’s memory.
Keepalive Connections
Enabling HTTP keep-alive connections reduces the overhead of establishing new TCP connections for each request. The keepalive_timeout directive specifies how long an idle keep-alive connection will remain open. A value between 60 and 120 seconds is often a good balance. keepalive_requests limits the number of requests that can be served over a single keep-alive connection; setting it to a high value like 1000 can further reduce overhead.
Buffering and Caching
Nginx buffering can significantly improve performance by allowing it to handle requests and responses more efficiently. client_body_buffer_size and client_header_buffer_size control the buffer sizes for client request bodies and headers, respectively. For large file uploads, you might need to increase client_body_buffer_size. proxy_buffering should generally be on when proxying to your application server (Gunicorn/FPM) to allow Nginx to buffer responses from the backend, improving client throughput. proxy_buffer_size and proxy_buffers control the size and number of these buffers.
Gzip Compression
Enabling Gzip compression can drastically reduce the amount of data transferred over the network, leading to faster page loads. Ensure you configure gzip_types to include common MIME types served by your Laravel application, such as text/html, text/css, application/javascript, and application/json. gzip_comp_level controls the compression level (1-9), with 6 being a good default for balancing compression ratio and CPU usage.
Example Nginx Configuration Snippet
Here’s a consolidated example for a typical Laravel setup:
worker_processes auto;
events {
worker_connections 4096; # Adjust based on server resources and expected load
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
# Buffering settings
client_body_buffer_size 10m; # For larger file uploads, adjust as needed
client_header_buffer_size 1k;
large_client_header_buffers 4 32k;
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 8 128k;
proxy_busy_buffers_size 256k;
# 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 image/svg+xml;
# SSL Configuration (if applicable)
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_prefer_server_ciphers on;
# ssl_session_cache shared:SSL:10m;
# ssl_session_timeout 10m;
server {
listen 80;
# listen 443 ssl http2; # Uncomment for SSL
server_name your_domain.com;
root /var/www/your_laravel_app/public;
index index.php index.html index.htm;
location / {
try_files $uri /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi_params.conf;
# Adjust fastcgi_pass based on your PHP-FPM configuration
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_read_timeout 300; # Increase for long-running PHP scripts
}
# Deny access to hidden files
location ~ /\.ht {
deny all;
}
# Caching for static assets (adjust cache duration as needed)
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, no-transform";
access_log off;
}
}
}
Gunicorn/PHP-FPM Tuning for Laravel
The application server (Gunicorn for Python/Flask/Django, or PHP-FPM for PHP/Laravel) is where your application code actually runs. Tuning these is critical for handling requests efficiently and preventing bottlenecks.
Gunicorn Tuning (Python)
Gunicorn is a popular WSGI HTTP Server for Python. Key tuning parameters include the number of worker processes and the worker type.
Worker Processes
The number of worker processes directly impacts concurrency. A common recommendation is (2 * number_of_cores) + 1. This formula aims to keep CPU cores busy while accounting for I/O waits.
Worker Type
Gunicorn supports several worker types:
sync: The default and simplest worker type. It handles one request at a time per worker. Not ideal for I/O-bound applications.eventlet,gevent: Asynchronous worker types that use green threads to handle many requests concurrently within a single process. These are generally preferred for I/O-bound applications like web services that make external API calls or database queries.
For most Laravel-like applications (even if running on Python frameworks), using gevent or eventlet workers is highly recommended for better concurrency and resource utilization.
Example Gunicorn Command Line
gunicorn --workers 3 --worker-class gevent --bind 0.0.0.0:8000 your_project.wsgi:application
In this example, --workers 3 is set for a 2-core CPU, and --worker-class gevent is used for asynchronous handling. Adjust --workers based on your CPU count and application’s I/O characteristics. The --bind address and port should match what Nginx is configured to proxy to.
PHP-FPM Tuning (PHP/Laravel)
PHP-FPM (FastCGI Process Manager) is the standard way to run PHP applications. Its configuration is primarily managed in php-fpm.conf and pool configuration files (e.g., www.conf).
Process Management Modes
PHP-FPM offers three process management modes:
static: A fixed number of child processes are spawned when the FPM master process starts. This offers the most predictable performance but can be inefficient if traffic fluctuates wildly.dynamic: The number of child processes varies based on traffic. It starts with a minimum number and spawns more up to a maximum as needed. This is a good balance for most applications.ondemand: Child processes are spawned only when a request is received. This saves memory but can introduce latency for the first request after a period of inactivity.
For most Laravel applications, the dynamic mode is a good starting point. You’ll tune parameters like pm.max_children, pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers.
Tuning Parameters for Dynamic Mode
pm.max_children: The maximum number of child processes that can be active at the same time. This is the most critical setting and should be determined by your server’s available RAM. A common formula is (Total RAM - RAM used by OS/Nginx) / Average RAM per PHP process. Monitor memory usage closely.
pm.start_servers: The number of child processes to start when the FPM master process is started.
pm.min_spare_servers: The minimum number of idle supervisor processes. If there are fewer idle processes than this number, the master process will fork more child processes.
pm.max_spare_servers: The maximum number of idle supervisor processes. If there are more idle processes than this number, the master process will terminate some of them.
Example PHP-FPM Pool Configuration (www.conf)
[www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock # Match this with Nginx's fastcgi_pass listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 100 ; Adjust based on server RAM (e.g., 100 * 50MB/process = 5GB) pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.process_idle_timeout = 10s pm.max_requests = 500 ; Restart processes after X requests to prevent memory leaks request_terminate_timeout = 120 ; Timeout for individual PHP requests (seconds) request_slowlog_timeout = 30 ; Log requests slower than this (seconds) slowlog = /var/log/php/php8.1-fpm.slow.log catch_workers_output = yes catch_workers_output_level = notice
Important: After modifying PHP-FPM configuration, you must restart the PHP-FPM service:
sudo systemctl restart php8.1-fpm
PostgreSQL Tuning for Laravel
Database performance is often the ultimate bottleneck. Tuning PostgreSQL, especially on AWS RDS or EC2, requires careful consideration of memory, I/O, and query optimization.
Key PostgreSQL Configuration Parameters
These parameters are typically set in postgresql.conf. On AWS RDS, you’ll manage these via Parameter Groups.
shared_buffers
This is the most important parameter. It defines the amount of memory dedicated to caching data blocks read from disk. A common recommendation is 25% of your total system RAM, but this can be higher (up to 40%) on dedicated database servers with ample RAM. For RDS, this is often set automatically based on instance size, but you can override it.
work_mem
This parameter controls the amount of memory that can be used for internal sort operations and hash tables before writing to temporary disk files. If your queries involve large sorts or joins, increasing work_mem can significantly speed them up. However, be cautious: this memory is allocated *per operation*, so a high value combined with many concurrent queries can exhaust RAM. Start with a moderate value (e.g., 16MB-64MB) and monitor query performance and memory usage.
maintenance_work_mem
This is the maximum memory to be used for maintenance operations like VACUUM, CREATE INDEX, and ALTER TABLE. A higher value can speed up these operations. A common recommendation is 10-25% of total RAM, but it doesn’t need to be as high as shared_buffers.
effective_cache_size
This parameter informs the query planner about how much memory is available for disk caching by the operating system and PostgreSQL’s shared buffers. It doesn’t allocate memory itself but influences the planner’s decisions. A good starting point is 50-75% of total RAM.
max_connections
The maximum number of concurrent connections allowed. This should be set based on your application’s needs and server capacity. Too high a value can lead to excessive memory consumption. Ensure your application connection pooler (if used) is configured appropriately.
wal_buffers
Memory for Write-Ahead Log (WAL) information. A value of -1 (auto) is often sufficient, but setting it to a reasonable value like 16MB can sometimes improve write performance.
random_page_cost and seq_page_cost
These parameters influence the query planner’s cost estimates for different types of disk I/O. On modern SSDs (common on AWS), random I/O is much faster than traditional HDDs. Lowering random_page_cost (e.g., from 4.0 to 1.1 or 1.5) can encourage the planner to use index scans more often, which can be beneficial for read-heavy workloads.
Example PostgreSQL Configuration Snippet (for a 32GB RAM instance)
# Memory Settings shared_buffers = 8GB # ~25% of 32GB RAM work_mem = 64MB # Start here, monitor queries maintenance_work_mem = 4GB # ~12.5% of 32GB RAM effective_cache_size = 24GB # ~75% of 32GB RAM # Connection Settings max_connections = 200 # Adjust based on application needs and server capacity # WAL Settings wal_buffers = 16MB wal_writer_delay = 200ms # Default is 200ms, can be tuned commit_delay = 10ms # Can improve performance for high-concurrency writes commit_siblings = 5 # Number of concurrent transactions to wait for before committing # Planner Cost Settings (for SSDs) random_page_cost = 1.5 # Lower for SSDs seq_page_cost = 1.0 # Default # Logging log_min_duration_statement = 1000 # Log statements longer than 1 second log_statement = 'none' # 'all', 'ddl', 'mod', 'none' log_lock_waits = on log_temp_files = 0 # Log temporary files larger than this size (0 to disable) # Autovacuum Tuning (Crucial for performance and preventing bloat) autovacuum = on autovacuum_max_workers = 3 # Number of worker processes autovacuum_naptime = 1min # How often to check for jobs autovacuum_vacuum_threshold = 50 # Minimum number of row updates before vacuum autovacuum_analyze_threshold = 50 # Minimum number of row updates before analyze autovacuum_vacuum_scale_factor = 0.1 # Percentage of table size to vacuum autovacuum_analyze_scale_factor = 0.05 # Percentage of table size to analyze
Note on AWS RDS: When using RDS, you’ll apply these settings via a custom Parameter Group. Changes to shared_buffers, work_mem, and other memory-intensive parameters often require an instance reboot to take effect.
Monitoring and Iteration
Tuning is an iterative process. Use monitoring tools to observe your system’s behavior under load:
- Nginx: Use
stub_statusmodule for active connections, requests per second, etc. - Gunicorn/PHP-FPM: Monitor process counts, memory usage, and request latency. PHP-FPM has its own status page.
- PostgreSQL: Utilize tools like
pg_stat_activity,pg_stat_statements(requires extension), CloudWatch metrics (for RDS), and general OS-level monitoring (CPU, RAM, I/O).
Regularly review logs for errors, slow queries, and performance warnings. Adjust parameters incrementally and measure the impact. What works for one application might not work for another, so continuous profiling and tuning are key to maintaining optimal performance on AWS.