The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on Linode for Laravel
Nginx as a High-Performance Frontend Proxy
For a Laravel application, Nginx serves as the ideal frontend proxy. Its event-driven, asynchronous architecture makes it exceptionally efficient at handling concurrent connections, serving static assets, and buffering requests before they reach your application server (Gunicorn for Python/Flask/Django, or PHP-FPM for PHP/Laravel). The key is to configure Nginx to offload as much work as possible and to pass requests to the backend efficiently.
Nginx Configuration for Laravel
We’ll focus on a production-ready Nginx configuration. This involves optimizing worker processes, caching, compression, and secure SSL termination. Assume your Laravel application is running on a Linode instance, and you’re using a domain name pointing to its IP address.
Core Nginx Settings
Edit your main Nginx configuration file, typically located at /etc/nginx/nginx.conf. These global settings affect all worker processes.
Worker Processes: Set worker_processes to the number of CPU cores available on your server. This allows Nginx to utilize all available processing power.
Worker Connections: worker_connections defines the maximum number of simultaneous connections that each worker process can handle. A common starting point is 1024 or higher, depending on your expected traffic. The total maximum connections will be worker_processes * worker_connections.
Event Handling: Use the epoll event method on Linux for optimal performance.
File Descriptors: Increase the worker_rlimit_nofile to a sufficiently high number to avoid “too many open files” errors under heavy load.
Example nginx.conf Snippet
Add or modify these directives in your /etc/nginx/nginx.conf:
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto; # Set to the number of CPU cores, or 'auto'
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on expected load and server resources
multi_accept on;
use epoll;
}
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 image/svg+xml;
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# Include virtual host configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Laravel Site Configuration
Create a new server block configuration file for your Laravel application, typically in /etc/nginx/sites-available/your-app.conf and then symlink it to /etc/nginx/sites-enabled/.
This configuration will handle:
- SSL termination (using Let’s Encrypt/Certbot).
- Serving static assets directly.
- Proxying dynamic requests to PHP-FPM.
- Setting appropriate headers for security and performance.
Example your-app.conf (with PHP-FPM)
# /etc/nginx/sites-available/your-app.conf
# Redirect HTTP to HTTPS
server {
listen 80;
server_name your-domain.com www.your-domain.com;
return 301 https://$host$request_uri;
}
# HTTPS Server Block
server {
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
# SSL Configuration (obtained via Certbot)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Root directory for your Laravel application
root /var/www/your-app/public;
index index.php index.html index.htm;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; # Ensure you have valid SSL for all subdomains
# Cache static assets for a year
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
access_log off;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Handle all other requests to Laravel's front controller
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass PHP scripts to PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Adjust socket path based on your PHP-FPM configuration
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Example for PHP 8.1
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Prevent access to sensitive files
location ~ ^/(composer\.json|composer\.lock|storage|bootstrap/cache|\.env) {
deny all;
}
# Optimize PHP-FPM buffer sizes if needed
# fastcgi_buffers 8 16k;
# fastcgi_buffer_size 32k;
}
After creating the file, enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/your-app.conf /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
PHP-FPM Tuning for Laravel
PHP-FPM (FastCGI Process Manager) is the de facto standard for running PHP applications like Laravel. Its performance is critical. We’ll tune its process management and resource allocation.
PHP-FPM Configuration Files
The primary configuration file is usually /etc/php/[version]/fpm/php-fpm.conf. Pool configurations are in /etc/php/[version]/fpm/pool.d/www.conf (or a custom pool name).
Process Management: Dynamic vs. Static
The most impactful setting is the process manager. For most Laravel applications on Linode, the dynamic process manager is a good balance. It scales the number of FPM workers based on demand, saving resources when idle.
Key directives in pool.d/www.conf:
pm = dynamic: Use dynamic process management. Other options arestatic(fixed number of workers) andondemand(spawns workers only when a request comes in).pm.max_children: The maximum number of FPM child processes that can be spawned. This is the most critical setting. It should be calculated based on your server’s RAM and the memory footprint of your Laravel application. A common formula is(Total RAM - RAM for OS/Nginx/DB) / Average PHP Process Memory.pm.start_servers: The number of FPM processes to start when the FPM master process is started.pm.min_spare_servers: The minimum number of FPM processes to keep idle.pm.max_spare_servers: The maximum number of FPM processes to keep idle.pm.process_idle_timeout: The number of seconds after which an idle process will be killed.pm.max_requests: The number of requests each child process should execute before respawning. This helps prevent memory leaks. A value between 500 and 1000 is typical.
Example pool.d/www.conf Tuning
; /etc/php/8.1/fpm/pool.d/www.conf (Example for PHP 8.1) [www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock ; Ensure this matches Nginx config listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 100 ; Adjust based on server RAM and app memory usage pm.start_servers = 10 ; Initial number of workers pm.min_spare_servers = 5 ; Minimum idle workers pm.max_spare_servers = 20 ; Maximum idle workers pm.max_requests = 750 ; Respawn after this many requests pm.process_idle_timeout = 10s ; Kill idle processes after 10 seconds ; Other useful settings request_terminate_timeout = 60s ; Timeout for individual PHP scripts ; rlimit_files = 4096 ; Increase if needed, often handled by OS limits ; rlimit_core = 0 ; Disable core dumps for production
After modifying the PHP-FPM pool configuration, restart the service:
sudo systemctl restart php8.1-fpm # Adjust version as needed
Monitoring PHP-FPM Memory Usage
Use tools like htop or ps aux | grep php-fpm to monitor the memory consumption of your PHP-FPM processes. If your server is constantly swapping or running out of memory, you need to reduce pm.max_children. If you have plenty of free RAM and your application feels sluggish under load, you might be able to increase it.
MongoDB Performance Tuning on Linode
MongoDB’s performance is heavily influenced by its configuration, hardware, and workload. On Linode, you have control over these factors. We’ll focus on key configuration parameters and operational best practices.
Key MongoDB Configuration Parameters
The main configuration file is typically /etc/mongod.conf.
Storage Engine
Ensure you are using the WiredTiger storage engine, which is the default and recommended for most workloads. It offers excellent compression and concurrency.
# /etc/mongod.conf
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger # Explicitly set, though usually default
wiredTiger:
engineConfig:
cacheSizeGB: 0.75 # Allocate 75% of available RAM to WiredTiger cache
collectionConfig:
blockSize: 64KB # Default, adjust if specific needs arise
indexConfig:
prefixCompression: true
cacheSizeGB: This is arguably the most critical setting. Allocate a significant portion of your Linode instance’s RAM to the WiredTiger cache. A common recommendation is 50-75% of available RAM. For a 4GB Linode, you might set this to 2 or 3 GB. Monitor your system’s RAM usage; if MongoDB is consuming too much and causing swapping, reduce this value.
Network and Connections
Configure the network interface and maximum connections.
# /etc/mongod.conf net: port: 27017 bindIp: 0.0.0.0 # Or specific IP if restricting access maxIncomingConnections: 2000 # Adjust based on expected load and server resources
maxIncomingConnections: Set this to a value that can accommodate your expected concurrent connections. Ensure your Linode’s firewall and MongoDB’s access control are properly configured.
Logging
Configure logging for easier debugging and performance analysis.
# /etc/mongod.conf
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
verbosity: 0 # 0 for normal, higher for more verbose logging (e.g., for debugging)
quiet: false
component:
storage:
logLevel: 0
command:
logLevel: 0
index:
logLevel: 0
query:
logLevel: 0
Query and Index Optimization
This is crucial for application performance. MongoDB’s query optimizer relies on indexes to efficiently retrieve data. Poorly indexed queries are a common bottleneck.
Identifying Slow Queries
Enable the slow query log in MongoDB. Set a threshold (e.g., 100ms) for queries to be logged.
# /etc/mongod.conf operationProfiling: mode: "slowOp" # or "all" for profiling all operations slowOpThresholdMs: 100 # Log operations slower than 100ms
After applying this change, restart MongoDB:
sudo systemctl restart mongod
Analyze the MongoDB logs (/var/log/mongodb/mongod.log) for slow operations. Use the explain() method in the MongoDB shell to understand query execution plans and identify missing indexes.
Example: Using explain()
// Connect to your MongoDB instance
// mongo
// Example query that might be slow
db.users.find({ email: "[email protected]" })
// Explain the query execution plan
db.users.find({ email: "[email protected]" }).explain("executionStats")
Look for:
totalKeysExaminedvs.totalDocsExamined: Ideally, these should be close. A large difference indicates inefficient scanning.winningPlan.stage: Should ideally beIXSCAN(index scan) rather thanCOLLSCAN(collection scan).
If a COLLSCAN is present or totalKeysExamined is high, you likely need an index. For the example above, an index on the email field would be beneficial:
db.users.createIndex({ email: 1 })
Monitoring and Maintenance
Regularly monitor MongoDB’s performance using tools like:
mongostat: Provides real-time server statistics.mongotop: Shows per-collection read/write activity.- Linode’s built-in monitoring for CPU, RAM, and disk I/O.
Perform regular backups and consider periodic index rebuilding or repair if you encounter data corruption issues (though this is rare with WiredTiger).