The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on AWS for Laravel
Nginx Configuration for Laravel Applications
Optimizing Nginx is crucial for serving Laravel applications efficiently, especially under load. We’ll focus on key directives that impact performance and security. This assumes a standard setup with Nginx acting as a reverse proxy to your PHP application server (Gunicorn for PHP-FPM via sockets, or directly if using PHP-FPM’s TCP mode).
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 determine the optimal number based on available CPU cores. worker_connections defines the maximum number of simultaneous connections that each worker process can handle. The total maximum connections will be worker_processes * worker_connections. Ensure this value is sufficiently high to handle your expected traffic, but not so high that it exhausts system resources.
A good starting point for a typical AWS EC2 instance (e.g., m5.large with 2 vCPUs) would be:
worker_processes auto;
events {
worker_connections 4096; # Adjust based on expected load and system limits
multi_accept on;
}
Buffering and Timeouts
Nginx buffering can significantly impact performance. By default, Nginx buffers responses from the upstream server. For APIs or applications that stream data, disabling or tuning these buffers can reduce latency. However, for typical web applications, appropriate buffering can help smooth out traffic spikes and reduce the load on the backend.
client_body_buffer_size: Sets the size of the buffer used for reading client request body. If the request body is larger than this size, the entire body or the remainder of it will be written to a temporary file. A common value is 128k.
proxy_buffers and proxy_buffer_size: These control buffering for responses from the upstream server. For most Laravel apps, default values are often fine, but for high-throughput APIs, you might consider increasing them or disabling buffering if appropriate.
proxy_connect_timeout, proxy_send_timeout, proxy_read_timeout: These timeouts are critical. Setting them too low can lead to premature timeouts for legitimate long-running requests. Setting them too high can tie up worker connections during slow requests. For Laravel, especially with background jobs, consider values between 60s and 300s, depending on your application’s typical request durations.
http {
# ... other http directives ...
client_body_buffer_size 128k;
proxy_buffers 8 16k; # Example: 8 buffers of 16KB each
proxy_buffer_size 16k;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # Allow longer read timeouts for potentially slow operations
# ... rest of http block ...
}
Gzip Compression
Enabling Gzip compression is a low-hanging fruit for performance gains. It significantly reduces the size of text-based assets (HTML, CSS, JS, JSON) transferred over the network. Ensure you’re only compressing compressible content types and not already compressed assets like images.
http {
# ...
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Compression level (1-9, 6 is a good balance)
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_min_length 1000; # Don't compress small files
gzip_disable "msie6"; # Disable for older IE versions if necessary
# ...
}
Caching and Static File Serving
Nginx is excellent at serving static files directly, bypassing your PHP application. Configure long cache expiry headers for assets that don’t change frequently. This offloads significant work from your application server.
server {
# ...
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
log_not_found off;
}
# ...
}
PHP-FPM Configuration (with Gunicorn/Uvicorn for PHP-FPM)
When using Gunicorn or Uvicorn to manage PHP-FPM processes (a common pattern for PHP applications that need WSGI-like process management), tuning PHP-FPM itself is critical. We’ll focus on the php-fpm.conf and pool.d/www.conf files.
Process Management (pm)
PHP-FPM offers three process management modes: static, dynamic, and ondemand. For production, static is often preferred for predictable performance and resource usage, as it keeps a fixed number of FPM workers running. dynamic can be good for variable loads but might introduce slight latency when scaling up.
pm.max_children: The maximum number of child processes that will be spawned. This is the most critical setting. It should be tuned based on your server’s RAM and the memory footprint of your Laravel application. A common formula is (Total RAM - RAM for OS/Nginx) / Average PHP Process Memory. 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 (spare) processes that should be kept active.
pm.max_spare_servers: The maximum number of idle (spare) processes that should be kept active.
Example pool.d/www.conf for a server with 8GB RAM, where Laravel processes consume ~50MB each:
; /etc/php/8.1/fpm/pool.d/www.conf (or similar path) [www] user = www-data group = www-data listen = /run/php/php8.1-fpm.sock # Or TCP: 127.0.0.1:9000 listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = static pm.max_children = 100 ; (8GB RAM - 1GB OS/Nginx) / 50MB = ~140. Start lower and tune. pm.start_servers = 20 pm.min_spare_servers = 10 pm.max_spare_servers = 30 request_terminate_timeout = 300s ; Match Nginx proxy_read_timeout or slightly higher rlimit_files = 4096 rlimit_nofile = 4096 catch_workers_output = yes ; Useful for debugging, disable in production for performance ; php_admin_value[memory_limit] = 256M ; Set per-pool if needed, but better managed globally or via .htaccess ; php_admin_value[max_execution_time] = 300
Gunicorn/Uvicorn Configuration
If you’re using Gunicorn (Python WSGI HTTP Server) to proxy requests to PHP-FPM (e.g., via a custom worker or a tool like gunicorn-php), its configuration also matters. The key is to ensure it can handle the connection load and doesn’t become a bottleneck.
--workers: The number of worker processes. A common recommendation is (2 * number_of_cores) + 1. However, since these workers are primarily I/O bound waiting for PHP-FPM, you might need more workers than this formula suggests, especially if PHP-FPM is also a bottleneck.
--worker-connections: (Not a direct Gunicorn setting, but implied by the number of workers and threads if used). Gunicorn’s default is often sufficient if you’re not using threads.
--timeout: The maximum time in seconds that a worker can spend on a request. This should align with proxy_read_timeout in Nginx and request_terminate_timeout in PHP-FPM.
--keep-alive: The number of seconds to wait for requests on a persistent connection. A value of 2 or 5 is usually sufficient.
# Example Gunicorn command for PHP-FPM socket # Assuming you have a PHP-FPM socket and a Gunicorn worker for PHP # This is a conceptual example; actual implementation depends on the PHP worker used. # If using a direct PHP-FPM worker (e.g., gunicorn-php) gunicorn --bind 0.0.0.0:8000 --workers 5 --timeout 300 --keep-alive 5 my_php_app:app # If Gunicorn is proxying to PHP-FPM via Nginx (less common for direct PHP-FPM use) # This scenario is more typical for Python apps, but illustrates Gunicorn settings. # For PHP, Nginx -> PHP-FPM is more direct.
Note: The direct use of Gunicorn/Uvicorn for PHP applications is less common than using Nginx directly with PHP-FPM. If you are using a tool that *integrates* Gunicorn with PHP-FPM, ensure you understand how that integration works. Often, Nginx is configured to proxy to PHP-FPM directly via its socket or TCP port.
MySQL Tuning on AWS RDS/EC2
Database performance is often the ultimate bottleneck. Tuning MySQL, especially on AWS RDS, involves understanding instance types, parameter groups, and query optimization.
RDS Instance Sizing
Choose an instance class that provides sufficient CPU, RAM, and Network Bandwidth for your workload. Memory-optimized instances (like r6g, r5, x1) are generally excellent for databases due to their large RAM capacity, which allows for more data to be cached. Network performance is also critical; Graviton instances (g suffix) often offer superior network throughput at a lower cost.
MySQL Parameter Groups
AWS RDS uses parameter groups to manage MySQL configuration. You’ll need to create a custom parameter group to modify these settings. Key parameters to tune:
innodb_buffer_pool_size: This is the most critical parameter for InnoDB performance. It’s the memory area where InnoDB caches table and index data. Aim to set this to 70-80% of the instance’s available RAM. For example, on anr5.xlarge(16 GiB RAM), you might set this to 10-12 GiB.innodb_log_file_size: Controls the size of the redo log files. Larger log files can improve write performance by reducing the frequency of log flushing, but increase recovery time after a crash. A common starting point is512Mor1G.innodb_flush_log_at_trx_commit: Controls how often InnoDB flushes log buffers to disk.1(default) provides full ACID compliance but can be slow.2flushes to OS buffer but syncs to disk every second, offering a good balance for many applications.0is fastest but risks data loss on crash. For Laravel,2is often a safe and performant choice.max_connections: The maximum number of simultaneous client connections. Set this based on your application’s needs and instance capacity. Too high can exhaust memory.query_cache_sizeandquery_cache_type: The query cache is deprecated in MySQL 8.0 and removed in 8.0.17. If using an older version, it can sometimes help, but often causes contention. For modern Laravel apps, it’s best to disable it (query_cache_size=0,query_cache_type=0) and rely on application-level caching (Redis, Memcached) and proper indexing.tmp_table_sizeandmax_heap_table_size: These control the maximum size of in-memory temporary tables. Increasing them can speed up complex queries that require temporary tables, but ensure they don’t exceed available RAM.
Example custom parameter group settings (adjust values based on your RDS instance size):
-- These are conceptual SQL commands to illustrate parameter values. -- Actual modification is done via AWS RDS Console/CLI for Parameter Groups. -- Parameter: innodb_buffer_pool_size -- Value: 10G (for a 16GiB RAM instance like r5.xlarge) -- Parameter: innodb_log_file_size -- Value: 1G -- Parameter: innodb_flush_log_at_trx_commit -- Value: 2 -- Parameter: max_connections -- Value: 200 (adjust based on application needs and instance RAM) -- Parameter: query_cache_size -- Value: 0 -- Parameter: query_cache_type -- Value: 0 -- Parameter: tmp_table_size -- Value: 64M -- Parameter: max_heap_table_size -- Value: 64M
Query Optimization and Indexing
No amount of server tuning can fix poorly written queries. Regularly analyze your application’s database queries:
- Enable the slow query log in MySQL to identify queries exceeding a certain execution time.
- Use
EXPLAINon slow queries to understand their execution plan and identify missing indexes. - Ensure foreign keys are indexed.
- Avoid
SELECT *; select only the columns you need. - Be cautious with
JOINs on large tables without proper indexing. - Use application-level caching (Redis/Memcached) for frequently accessed, relatively static data.
Example of enabling and analyzing the slow query log:
-- In your RDS custom parameter group: -- Parameter: slow_query_log -- Value: 1 -- Parameter: long_query_time -- Value: 2 (seconds) -- Parameter: log_output -- Value: FILE (or TABLE if preferred and supported) -- After enabling, queries slower than 2 seconds will be logged. -- You can then analyze the log file (via RDS console/CLI) or query the 'mysql.slow_log' table. -- Example EXPLAIN statement: EXPLAIN SELECT users.name, orders.order_date FROM users JOIN orders ON users.id = orders.user_id WHERE users.created_at BETWEEN '2023-01-01' AND '2023-12-31';
AWS Specific Considerations
When deploying on AWS, consider:
- Instance Types: Choose compute-optimized (
cseries) for CPU-bound tasks, memory-optimized (rseries) for memory-intensive applications (like databases or caching layers), and general-purpose (mseries) for balanced workloads. Graviton instances (gsuffix) offer excellent price-performance. - EBS Volumes: For databases, use Provisioned IOPS SSD (
io1/io2) or General Purpose SSD (gp3) volumes.gp3is often the best balance of cost and performance, allowing independent tuning of IOPS and throughput. - Network Bandwidth: Ensure your instance types and EBS configurations provide sufficient network throughput for database connections and inter-service communication.
- Security Groups: Configure strict security group rules to only allow necessary traffic between your Nginx, application, and database layers.
- Monitoring: Leverage CloudWatch extensively. Monitor CPU utilization, memory usage (if available via Enhanced Monitoring), network I/O, disk I/O, database connections, and query latency. Set up alarms for critical metrics.
Conclusion
This playbook provides a solid foundation for tuning your Laravel stack on AWS. Remember that performance tuning is an iterative process. Start with these configurations, monitor your application’s behavior under load, and make adjustments based on real-world data. Always test changes in a staging environment before deploying to production.