The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on DigitalOcean for Laravel
Nginx as a High-Performance Frontend Proxy
Nginx is the de facto standard for serving web applications due to its event-driven, asynchronous architecture, making it exceptionally efficient at handling concurrent connections. When deploying Laravel applications, Nginx acts as a robust reverse proxy, efficiently serving static assets and forwarding dynamic requests to your application server (Gunicorn for Python, or PHP-FPM for PHP).
A critical aspect of Nginx tuning for Laravel involves optimizing worker processes, connection handling, and caching strategies. For a DigitalOcean Droplet, we’ll start by adjusting the worker_processes directive. A common best practice is to set this to the number of CPU cores available on your server. You can determine this using the nproc command.
Determining Optimal Worker Processes
Execute the following command on your Droplet:
nproc
Let’s assume nproc returns 4. We’ll then configure Nginx accordingly.
Nginx Configuration Tuning
Edit your main Nginx configuration file, typically located at /etc/nginx/nginx.conf. We’ll focus on the events and http blocks.
user www-data;
worker_processes 4; # Set to the output of 'nproc'
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024; # Adjust based on expected load and server memory
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
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip compression for dynamic content
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# SSL configuration (if applicable)
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_prefer_server_ciphers on;
# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
# Logging configuration
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Include virtual host configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Within your Laravel application’s server block (e.g., in /etc/nginx/sites-available/your_laravel_app), ensure efficient handling of static assets and proper proxying to your application server. For static files, leverage Nginx’s ability to serve them directly, bypassing the application server entirely.
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;
}
# Serve static assets directly
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d; # Cache static assets for 30 days
add_header Cache-Control "public";
access_log off; # Don't log access for static files
}
# Pass PHP requests to PHP-FPM or Gunicorn
# Example for PHP-FPM:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and socket path
# fastcgi_pass 127.0.0.1:9000; # Or TCP socket
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Example for Gunicorn (if using Python/Flask/Django with Nginx proxy)
# location / {
# proxy_pass http://unix:/run/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;
# }
# Deny access to hidden files
location ~ /\. {
deny all;
}
}
After making changes, always test your Nginx configuration and reload the service:
sudo nginx -t sudo systemctl reload nginx
Optimizing PHP-FPM or Gunicorn for Laravel
The application server is where your Laravel code executes. For PHP applications, PHP-FPM (FastCGI Process Manager) is the standard. For Python-based frameworks often deployed with Laravel-like structures, Gunicorn is a popular choice.
PHP-FPM Tuning
PHP-FPM’s performance is heavily influenced by its process management. The configuration file is typically /etc/php/X.Y/fpm/pool.d/www.conf (replace X.Y with your PHP version, e.g., 8.1).
; The number of child processes that will be spawned. ; This can be set to 'auto' to set it dynamically based on the number of CPUs. ; Default Value: -1 (means 1 to each CPU core) pm.max_children = 50 ; Adjust based on server memory and expected load ; The maximum number of children that can be spawned at the same time. ; Default Value: 5 pm.max_spawns = 10 ; The minimum number of children that should always be available. ; Default Value: 5 pm.min_spare_servers = 5 ; The maximum number of children that can be idle before they are killed. ; Default Value: 10 pm.max_spare_servers = 15 ; The script is executed with uid/gid of the pool. ; Default Value: run_as_user ; user = www-data ; group = www-data ; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'ip.add.re.ss:port', 'port', 'unix:/path/to/socket' ; Note: This value is mandatory. listen = /var/run/php/php8.1-fpm.sock ; Ensure this matches Nginx config ; Set to 'on' if you want to use CPU affinity. ; Default Value: off ; pm.affinity = off ; The number of requests each child process should execute before respawning. ; This can be useful to prevent memory leaks but can also slow down ; processing if set too low. ; Default Value: 0 (unlimited) pm.max_requests = 500 ; A good balance to prevent memory leaks ; Define the slowlog directory ; slowlog = /var/log/php/php8.1-fpm.slow.log ; request_slowlog_timeout = 10s
After modifying www.conf, restart PHP-FPM:
sudo systemctl restart php8.1-fpm # Adjust PHP version
Gunicorn Tuning (for Python/Django/Flask)
If you’re using Gunicorn to serve a Python application that integrates with or complements Laravel (e.g., a separate microservice), tuning is crucial. Gunicorn’s worker processes are key. The number of workers is typically calculated as (2 * Number of CPU Cores) + 1.
You can start Gunicorn with specific worker settings:
# Example command to start Gunicorn gunicorn --workers 3 --bind unix:/run/gunicorn.sock --chdir /path/to/your/python_app your_module:app
For more persistent deployments, consider using a process manager like systemd. A typical systemd service file for Gunicorn might look like this:
[Unit] Description=Gunicorn instance to serve your_python_app After=network.target [Service] User=your_user Group=your_group WorkingDirectory=/path/to/your/python_app ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/run/gunicorn.sock your_module:app # Or for TCP binding: # ExecStart=/usr/local/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 your_module:app Restart=always RestartSec=10 [Install] WantedBy=multi-user.target
Enable and start the service:
sudo systemctl enable your_python_app.service sudo systemctl start your_python_app.service
MongoDB Performance Tuning on DigitalOcean
MongoDB, a NoSQL document database, is often used with Laravel for its flexibility. Performance tuning involves optimizing memory usage, indexing, and query patterns.
Memory Allocation and WiredTiger
MongoDB’s default storage engine, WiredTiger, performs best when it can cache a significant portion of the working set in RAM. The storage.wiredTiger.engineConfig.cacheSizeGB parameter in /etc/mongod.conf is crucial. A common recommendation is to allocate 50% of system RAM to the WiredTiger cache, ensuring enough is left for the OS and other processes.
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 2 # Example: If Droplet has 4GB RAM, leave 2GB for OS/other services
collectionConfig:
cacheResource:
string: "UTF-8"
indexConfig:
prefixCompression: "disabled"
# ... other configurations ...
# Example: If Droplet has 8GB RAM, set to 4GB
# storage:
# wiredTiger:
# engineConfig:
# cacheSizeGB: 4
After modifying mongod.conf, restart MongoDB:
sudo systemctl restart mongod
Indexing Strategies
Proper indexing is paramount for MongoDB query performance. Analyze your application’s query patterns using the MongoDB profiler or tools like mongotop and mongostat. Identify slow queries and create appropriate indexes.
To enable the profiler:
use admin db.setProfilingLevel(1, 100) // Level 1, log queries slower than 100ms
To view slow queries:
db.system.profile.find().pretty()
Example of creating an index on a `users` collection for queries filtering by `email` and sorting by `created_at`:
use your_database_name
db.users.createIndex( { email: 1, created_at: -1 } )
For compound indexes, consider the order of fields based on query selectivity and sort order. Use tools like explain() to verify index usage.
db.collection.find({ field1: "value1", field2: "value2" }).explain()
Query Optimization and Schema Design
Avoid querying fields that are not indexed. Use projection to retrieve only the necessary fields, reducing network I/O and memory usage.
db.users.find( { status: "active" }, { name: 1, email: 1, _id: 0 } )
Denormalization can be beneficial for read-heavy workloads, embedding related data to reduce the need for joins (though MongoDB doesn’t have traditional joins, it refers to embedding documents). However, excessive embedding can lead to large documents and performance issues. Balance this with the application’s access patterns.
Putting It All Together: A Holistic Approach
This playbook provides a foundational set of optimizations for Nginx, PHP-FPM/Gunicorn, and MongoDB on DigitalOcean for Laravel applications. Remember that performance tuning is an iterative process. Continuously monitor your system’s metrics (CPU, RAM, I/O, network, application response times) using tools like Prometheus, Grafana, or DigitalOcean’s built-in monitoring. Adjust configurations based on observed behavior and load patterns. Regularly review and update your indexes and query strategies as your application evolves.