The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on AWS for Laravel
Nginx as a High-Performance Frontend for Laravel Applications
When deploying Laravel applications, Nginx serves as an excellent choice for a web server due to its event-driven architecture, making it highly efficient at handling concurrent connections. For PHP-based Laravel apps, we’ll typically pair Nginx with PHP-FPM. For Python-based applications using Gunicorn, Nginx acts as a reverse proxy.
Nginx Configuration for PHP-FPM (Laravel)
The core of Nginx configuration for PHP involves setting up a location block to handle PHP requests and pass them to the PHP-FPM process. This configuration assumes a standard PHP-FPM setup listening on a Unix socket or a TCP port.
Optimizing PHP-FPM Worker Processes
Tuning PHP-FPM’s process management is critical. The pm (process manager) setting dictates how workers are managed. For production, dynamic or ondemand are generally preferred over static to conserve resources when idle, but static can offer lower latency under consistent high load. Adjust pm.max_children, pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers based on your server’s CPU and RAM. A common starting point for a moderately sized EC2 instance (e.g., m5.large) might be:
PHP-FPM Pool Configuration Example (/etc/php/8.1/fpm/pool.d/www.conf)
; Choose how the process manager (pm) should take on the task of managing the processes. ; Available options: ; 'static' - the number of child processes is never changed. ; 'dynamic' - the number of child processes is adjusted dynamically. ; 'ondemand' - the number of child processes is adjusted on demand. pm = dynamic ; The default value is 'dynamic', and it depends on pm.max_children, ; pm.start_servers, pm.min_spare_servers and pm.max_spare_servers. ; If pm is set to 'static', this value is used directly. ; Default value: 0 pm.max_children = 100 ; These values are used when pm is set to 'dynamic', and are ignored when 'static' or 'ondemand'. ; Number of child processes to start when pm is set to 'dynamic' or 'ondemand'. ; Default value: 5 pm.start_servers = 10 ; Minimum number of child processes which should be kept active. ; Default value: 2 pm.min_spare_servers = 5 ; Maximum number of child processes which should be kept active. ; Default value: 5 pm.max_spare_servers = 20 ; The maximum number of requests each child process should execute before re-spawning. ; This can be useful to prevent memory leaks from accumulating over time. ; Default value: 0 (unlimited) pm.max_requests = 500 ; The TCP socket or the pipe address on which to listen. ; Default value: /run/php/php8.1-fpm.sock ; listen = /run/php/php8.1-fpm.sock ; If you are listening on a TCP socket, you can specify the address and port. ; listen = 127.0.0.1:9000
Nginx Server Block for Laravel
The Nginx server block should be configured to serve static assets directly and pass PHP requests to the PHP-FPM pool. Ensure fastcgi_read_timeout is set appropriately for long-running Laravel tasks (e.g., queue workers processing). A common value is 300 seconds, but this can be increased if necessary.
Nginx Site Configuration Example (/etc/nginx/sites-available/your_laravel_app)
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_laravel_app/public; # Adjust to your Laravel public directory
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass PHP scripts to FastCGI server
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php-fpm listening on a Unix socket:
fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Adjust to your PHP-FPM socket path
# With php-fpm listening on TCP/IP:
# fastcgi_pass 127.0.0.1:9000;
# Increase timeout for long-running scripts
fastcgi_read_timeout 300s; # 5 minutes
}
# Deny access to .htaccess files, if any
location ~ /\.ht {
deny all;
}
# Serve static files directly
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
access_log off;
}
# Prevent access to hidden files
location ~ /\. {
deny all;
}
}
Gunicorn Configuration for Python/Laravel (if applicable)
If your Laravel application is part of a larger Python ecosystem or uses Python for specific services, Gunicorn is a common WSGI HTTP Server. Nginx will act as a reverse proxy, forwarding requests to Gunicorn. The key is to configure Gunicorn’s worker processes and bind address.
Gunicorn Worker Processes and Threads
Gunicorn’s worker class and number of workers significantly impact performance. The sync worker class is the default and most basic. For I/O-bound applications, the gevent or eventlet worker classes (which use asynchronous I/O) can offer better concurrency. The number of workers is typically set to (2 * number_of_cores) + 1 as a starting point. Threads can be used with the gthread worker class.
Gunicorn Command Line Example
gunicorn --workers 4 --threads 2 --worker-class gevent --bind 0.0.0.0:8000 your_app.wsgi:application
In this example:
--workers 4: Sets 4 worker processes.--threads 2: Sets 2 threads per worker (if usinggthreadworker class).--worker-class gevent: Uses the gevent worker class for asynchronous I/O.--bind 0.0.0.0:8000: Binds Gunicorn to all network interfaces on port 8000.your_app.wsgi:application: Points to your application’s WSGI entry point.
Nginx Configuration for Gunicorn
Nginx will proxy requests to the Gunicorn server. It’s crucial to set appropriate timeouts and buffer sizes to prevent issues with long-running requests.
Nginx Server Block for Gunicorn
server {
listen 80;
server_name your_python_app.com www.your_python_app.com;
location / {
proxy_pass http://127.0.0.1:8000; # Matches Gunicorn's bind address
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;
# Timeouts for proxying
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s; # Crucial for long-running requests
}
# Serve static files directly if Gunicorn is not handling them
location /static/ {
alias /path/to/your/app/static/; # Adjust to your static files directory
expires 1y;
add_header Cache-Control "public";
}
location /media/ {
alias /path/to/your/app/media/; # Adjust to your media files directory
expires 1y;
add_header Cache-Control "public";
}
}
MongoDB Tuning for Laravel Applications
Optimizing MongoDB involves several aspects, from schema design to server configuration. For Laravel applications, common bottlenecks include inefficient queries, lack of proper indexing, and insufficient server resources.
Indexing Strategies
The most impactful optimization is proper indexing. Analyze your application’s queries using MongoDB’s explain() method. Identify slow queries in the MongoDB logs (if enabled) or via monitoring tools.
Identifying Slow Queries
# Connect to MongoDB mongo # Enable slow query logging (e.g., log queries slower than 100ms) db.setProfilingLevel(1, 100) # Perform application actions that trigger slow queries # View slow queries db.system.profile.find().pretty() # Disable profiling when done db.setProfilingLevel(0)
Once slow queries are identified, create appropriate indexes. For example, if you frequently query a `users` collection by `email` and `status`:
Creating Compound Indexes
db.users.createIndex( { email: 1, status: 1 } )
Consider the order of fields in compound indexes based on query selectivity and sort order. Use tools like mongodbatlas.com/tools/db-schema-advisor for schema analysis and index recommendations.
MongoDB Server Configuration (mongod.conf)
Key parameters in mongod.conf affect performance and stability. On AWS EC2 instances, ensure you’re using appropriate instance types (e.g., memory-optimized or storage-optimized) and EBS volumes (e.g., `gp3` or `io1` for higher IOPS).
Key `mongod.conf` Parameters
# mongod.conf
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
# For SSDs, consider WiredTiger with compression
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 0.75 # Example: 75% of available RAM, adjust based on system usage
# network interfaces
net:
port: 27017
bindIp: 0.0.0.0 # Or specific IPs for security
# logging
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
verbosity: 0 # Increase for debugging, decrease for production
# operation profiling
# profiling:
# slowOpThresholdMs: 100 # Log operations slower than 100ms
# mode: slowOp # Log only slow operations
# Sharding (if applicable)
# sharding:
# clusterRole: configsvr
# configsvrFile: /var/lib/mongodb/mongod.conf
# configdb: your_mongos_host:27019
# Security (essential for production)
# security:
# authorization: enabled
The cacheSizeGB parameter is crucial. It dictates the WiredTiger cache size. A common recommendation is to allocate 50-75% of the system’s RAM to the WiredTiger cache, leaving enough for the OS and other processes. Monitor memory usage closely.
Connection Pooling
Ensure your Laravel application is configured to use connection pooling for MongoDB. Libraries like jenssegers/mongodb typically handle this automatically, but verify the pool size settings. An insufficient pool size can lead to connection exhaustion under load.
Example (config/database.php for jenssegers/mongodb)
<?php
return [
// ... other configurations
'mongodb' => [
'driver' => 'mongodb',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 27017),
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
'options' => [
'database' => env('DB_DATABASE'), // This is the database name
'ssl' => env('DB_SSL', false),
'replicaSet' => env('DB_REPLICA_SET'),
'poolsize' => env('DB_POOL_SIZE', 5), // Default pool size, adjust as needed
],
],
// ... other configurations
];
Adjust poolsize based on your application’s concurrency and MongoDB’s connection limits. Monitor the number of active connections in MongoDB.
AWS Specific Considerations
On AWS, leverage managed services where possible (e.g., Amazon RDS for relational databases, Amazon ElastiCache for caching). For self-managed MongoDB, consider:
- EC2 Instance Types: Choose instance types that match your workload (e.g.,
m5for general purpose,r5for memory-intensive,i3for I/O intensive). - EBS Volumes: Use
gp3for a good balance of performance and cost, orio1/io2for demanding workloads requiring provisioned IOPS. - Network Configuration: Ensure security groups allow necessary traffic between your application servers and MongoDB instances. Use VPC endpoints for private connectivity.
- Monitoring: Utilize Amazon CloudWatch for metrics on CPU, memory, disk I/O, and network traffic. Set up alarms for critical thresholds.
For MongoDB, consider Amazon DocumentDB if you want a managed, highly available, and scalable MongoDB-compatible database service. It offloads operational burdens like patching, backups, and scaling.