The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on DigitalOcean for PHP
Nginx as a High-Performance Frontend Proxy
For a PHP application, Nginx serves as an exceptionally efficient frontend proxy and static file server. Its asynchronous, event-driven architecture excels at handling a high volume of concurrent connections with minimal resource overhead. We’ll focus on tuning Nginx for optimal performance when serving dynamic PHP requests via Gunicorn (for Python backends) or directly to PHP-FPM.
Core Nginx Configuration Tuning
The primary configuration file, typically located at /etc/nginx/nginx.conf, contains global directives. Key parameters to adjust for performance include:
worker_processes: Set this to the number of CPU cores available on your DigitalOcean droplet. This allows Nginx to utilize all available processing power.worker_connections: This defines the maximum number of simultaneous connections that each worker process can handle. A common starting point is1024or higher, depending on expected traffic. The actual limit is also constrained by the system’s open file descriptor limit.multi_accept: Setting this toonallows each worker process to accept as many new connections as possible, rather than just one at a time.keepalive_timeout: Controls how long an idle keep-alive connection will remain open. A value between65and75seconds is often a good balance between resource usage and client responsiveness.send_timeout: Sets the timeout for sending a response to the client.client_header_timeout: Sets the timeout for reading the client header.client_body_timeout: Sets the timeout for reading the client body.
Here’s an example snippet from nginx.conf:
user www-data;
worker_processes auto; # Or set to number of CPU cores
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096; # Increased from default
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hides Nginx version for security
# Gzip compression for static assets
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;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
# Include server blocks
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Optimizing PHP-FPM Configuration
When using PHP-FPM (FastCGI Process Manager), tuning its configuration is critical. The primary configuration file is typically /etc/php/[version]/fpm/php-fpm.conf, with pool configurations in /etc/php/[version]/fpm/pool.d/www.conf.
Process Management (pm)
The pm directive controls how PHP-FPM manages worker processes. The most common and recommended settings are:
pm = dynamic: This is the default and generally recommended. PHP-FPM will manage the number of child processes based onpm.max_children,pm.start_servers,pm.min_spare_servers, andpm.max_spare_servers.pm = static: All child processes are created during the FPM startup and remain active. This can offer slightly better performance for high-traffic sites as there’s no overhead for process spawning, but it consumes more memory. Use this with caution and ensure sufficient RAM.pm = ondemand: Processes are created only when a request is received and killed after a certain idle period. This is memory-efficient but can introduce latency for the first request after a period of inactivity.
For dynamic, the key parameters are:
pm.max_children: The maximum number of child processes that will be created. This is the most important setting. Set it based on your server’s RAM. A common formula is(Total RAM - RAM for OS/Nginx/DB) / Average RAM per PHP-FPM process. Monitor memory usage and adjust.pm.start_servers: The number of child processes to start when PHP-FPM starts.pm.min_spare_servers: The minimum number of idle processes to maintain.pm.max_spare_servers: The maximum number of idle processes to maintain.
For static:
pm.max_children: The fixed number of child processes.
Example www.conf snippet for dynamic PM:
; /etc/php/8.1/fpm/pool.d/www.conf (adjust version as needed) [www] user = www-data group = www-data listen = /run/php/php8.1-fpm.sock ; Or a TCP socket like 127.0.0.1:9000 ; Process management settings pm = dynamic pm.max_children = 150 ; Adjust based on RAM (e.g., 150 * ~20MB/process = ~3GB) pm.start_servers = 20 pm.min_spare_servers = 10 pm.max_spare_servers = 30 pm.process_idle_timeout = 10s ; For dynamic, kills idle processes beyond this ; Request handling request_terminate_timeout = 60s ; Timeout for a single script execution ; request_slowlog_timeout = 10s ; Enable slow log for debugging ; Other settings chdir = / catch_workers_output = yes ; php_admin_value[memory_limit] = 256M ; Example: override PHP.ini settings ; php_admin_flag[display_errors] = off ; For production
Nginx Configuration for PHP-FPM
Your Nginx server block (virtual host) needs to be configured to pass PHP requests to the PHP-FPM socket or TCP port. Ensure you have appropriate caching headers for static assets.
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_app/public; # Adjust to your application's public directory
index index.php index.html index.htm;
# Enable Gzip compression for text-based assets
gzip_static on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Cache static assets for a long time
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|webp|woff|woff2|ttf|eot|otf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf; # Standard Nginx snippet for FastCGI
# Use the correct socket path or IP:Port
fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Or: fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params; # Includes standard FastCGI parameters
}
# Deny access to hidden files
location ~ /\.ht {
deny all;
}
# Optional: SSL configuration
# listen 443 ssl;
# 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;
}
Gunicorn for Python Applications
When deploying Python web applications (e.g., Django, Flask) on DigitalOcean, Gunicorn is a popular WSGI HTTP Server. It’s robust and performs well when proxied by Nginx.
Gunicorn Configuration and Tuning
Gunicorn’s performance is primarily determined by its worker count and type. It’s typically run as a service managed by systemd.
Worker Types
sync: The default worker type. It’s simple but can block under heavy load. Each worker handles one request at a time.gevent: Uses greenlets for concurrency. It’s asynchronous and can handle many requests concurrently, making it suitable for I/O-bound applications. Requires installing thegeventlibrary.eventlet: Similar togevent, using greenlets for asynchronous I/O.tornado: Uses the Tornado IOLoop.
For most modern applications, gevent or eventlet offer superior concurrency and performance, especially if your application spends significant time waiting for database queries or external API calls.
Worker Count
A common recommendation for the number of workers is (2 * Number of CPU Cores) + 1. However, this is a starting point. For I/O-bound applications using asynchronous workers (like gevent), you might need more workers to keep the CPU busy while others are waiting for I/O. Monitor your application’s performance and resource utilization.
Gunicorn Systemd Service Example
Create a systemd service file (e.g., /etc/systemd/system/gunicorn.service) to manage your Gunicorn process.
# /etc/systemd/system/gunicorn.service
[Unit]
Description=gunicorn daemon for my_project
After=network.target
[Service]
User=my_app_user
Group=www-data
WorkingDirectory=/home/my_app_user/my_project
ExecStart=/home/my_app_user/my_project/venv/bin/gunicorn \
--workers 3 \
--worker-class gevent \
--bind unix:/run/gunicorn.sock \
--log-level info \
--access-logfile - \
--error-logfile - \
my_project.wsgi:application
# Adjust --workers and --worker-class based on your needs and server specs
# Example for 4 CPU cores with gevent: --workers 9 --worker-class gevent
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
After creating the file, enable and start the service:
sudo systemctl daemon-reload sudo systemctl start gunicorn sudo systemctl enable gunicorn sudo systemctl status gunicorn
Nginx Configuration for Gunicorn
Nginx will proxy requests to the Gunicorn socket specified in the service file.
server {
listen 80;
server_name your_python_app.com www.your_python_app.com;
client_max_body_size 4G; # Adjust if you handle large uploads
location /static/ {
alias /home/my_app_user/my_project/static/; # Serve static files directly
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /home/my_app_user/my_project/media/; # Serve media files directly
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
proxy_set_header Host $http_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;
proxy_pass http://unix:/run/gunicorn.sock; # Proxy to Gunicorn socket
}
}
MongoDB Performance Tuning on DigitalOcean
MongoDB’s performance is heavily influenced by its configuration, hardware, and query patterns. On DigitalOcean, choosing the right droplet type (e.g., CPU-optimized, Memory-optimized) is a good first step.
Key MongoDB Configuration Parameters
The main configuration file is typically /etc/mongod.conf. Key parameters for performance tuning include:
storage.wiredTiger.engineConfig.cacheSizeGB: This is arguably the most critical setting. It defines the maximum amount of RAM that the WiredTiger storage engine can use for its cache. A common recommendation is to allocate 50% of the system’s RAM to the WiredTiger cache, leaving the other 50% for the OS and other processes. For example, on a 16GB droplet, set this to8.storage.journal.enabled: Should generally betruefor durability. Disabling it can improve write performance but risks data loss on crashes.operationProfiling.mode: Set toslowOporallto enable profiling of slow queries. This is essential for identifying performance bottlenecks.net.bindIp: Ensure this is set correctly, usually to0.0.0.0to listen on all interfaces or a specific private IP if using a firewall.sharding.clusterRole: If running a sharded cluster, this defines the role of the mongod instance.
# /etc/mongod.conf
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
verbosity: 0 # 0 is default, higher values for more verbose logging
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 8 # Example: For a 16GB RAM droplet, allocate 8GB to cache
# For production, consider enabling profiling
# operationProfiling:
# mode: slowOp # or 'all'
# slowOpThresholdMs: 100 # Log operations slower than 100ms
net:
port: 27017
bindIp: 0.0.0.0 # Listen on all interfaces, or a specific IP
# Security settings (essential for production)
# security:
# authorization: enabled
# Replication settings (for replica sets)
# replication:
# replSetName: "rs0"
# Sharding settings (if applicable)
# sharding:
# clusterRole: shardsvr
# configsvr: false
Monitoring and Query Optimization
Effective MongoDB performance tuning relies heavily on monitoring and optimizing queries. Use the following tools and techniques:
mongotop: Provides a near real-time view of the read and write activity of collections.mongostat: Provides a summary of MongoDB server statistics, including operations, network traffic, and memory usage.explain(): The most crucial tool for query optimization. Rundb.collection.find(...).explain("executionStats")on your slow queries to understand how MongoDB is executing them. Look forCOLLSCAN(collection scan) and ensure indexes are being used effectively.- Indexes: Ensure appropriate indexes are created for your common query patterns. Use
db.collection.getIndexes()to view existing indexes. - Slow Query Log: If profiling is enabled, analyze the slow query log (often found in
/var/log/mongodb/mongod.logifoperationProfiling.modeis set) to identify problematic queries.
Example of using explain():
// Connect to your database
// use mydatabase;
// Example query that might be slow without an index
var slowQuery = { status: "active", createdAt: { $lt: new Date("2023-01-01") } };
// Analyze the query execution
db.mycollection.find(slowQuery).explain("executionStats");
/*
Look for:
- indexName: Should show an index being used.
- stage: Should ideally be IXSCAN (index scan) not COLLSCAN (collection scan).
- nReturned: Number of documents returned.
- executionTimeMillis: Time taken for execution.
- totalKeysExamined: Number of index keys scanned.
- totalDocsExamined: Number of documents scanned.
If indexName is null or stage is COLLSCAN, you likely need to create an index.
*/
// Example of creating an index
db.mycollection.createIndex({ status: 1, createdAt: -1 });
Putting It All Together: A Holistic Approach
Achieving peak performance on DigitalOcean involves a layered approach. Start with the foundational configurations for Nginx, your application server (Gunicorn/PHP-FPM), and your database (MongoDB). Then, iteratively tune based on real-world monitoring and profiling. Remember that optimal settings are highly dependent on your specific application’s workload, traffic patterns, and the chosen DigitalOcean droplet size.