The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on OVH for PHP
Nginx as a High-Performance Frontend for PHP Applications
When deploying PHP applications, especially those leveraging modern frameworks and APIs, Nginx serves as an exceptionally efficient frontend. Its asynchronous, event-driven architecture excels at handling a high volume of concurrent connections, offloading the heavy lifting from your application servers. This section details critical Nginx tuning parameters for PHP workloads, focusing on connection management and static file serving.
Optimizing Worker Processes and Connections
The core of Nginx performance lies in its worker processes. The number of worker processes should ideally match the number of CPU cores available on the server. This ensures that Nginx can fully utilize the available processing power without excessive context switching.
`worker_processes` and `worker_connections`
The `worker_processes` directive dictates how many worker processes Nginx will spawn. Setting this to `auto` is often a good starting point, allowing Nginx to detect the number of CPU cores. The `worker_connections` directive defines the maximum number of simultaneous connections that each worker process can handle. This value, combined with `worker_processes`, determines the total maximum connections Nginx can manage. A common recommendation is to set `worker_connections` to a value that allows for ample headroom, considering not just client connections but also connections to upstream servers (like Gunicorn or PHP-FPM).
Example `nginx.conf` Snippet
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto; # Or set to the number of CPU cores, e.g., worker_processes 4;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on server resources and expected load
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ... other http configurations ...
}
The `multi_accept on;` directive allows each worker to accept multiple new connections at once, further enhancing efficiency. `sendfile on;` and `tcp_nopush on;` are crucial for efficient static file serving by reducing system calls and improving data transfer rates.
Caching and Compression for Static Assets
Serving static assets (CSS, JS, images) directly from Nginx is significantly faster than having your PHP application handle them. Implementing browser caching and Gzip compression can drastically reduce bandwidth usage and improve perceived page load times.
Browser Caching Directives
The `expires` directive in Nginx tells the browser how long it should cache a particular resource. For static assets that rarely change, setting a long expiration time (e.g., `30d` for 30 days) is highly beneficial.
Gzip Compression
Enabling Gzip compression reduces the size of text-based assets (HTML, CSS, JS, JSON) before they are sent to the client. This requires enabling the `gzip` module and configuring appropriate compression levels and types.
Example `nginx.conf` Snippet for Static Assets
# Inside your http block or a specific server block
# Gzip Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Compression level (1-9)
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Browser Caching for Static Assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public";
access_log off; # Optionally disable access logs for static files
}
# Serve static files directly from Nginx
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass PHP requests to your application server (e.g., PHP-FPM)
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust to your PHP-FPM socket/address
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
Remember to reload Nginx after making configuration changes: `sudo systemctl reload nginx`.
Tuning Gunicorn for PHP (via PHP-FPM)
While Gunicorn is primarily a Python WSGI HTTP Server, the principles of tuning its worker processes are directly applicable when it’s used to proxy requests to PHP-FPM. The goal is to ensure sufficient worker capacity to handle incoming requests without overwhelming the PHP interpreter or the underlying server resources.
Gunicorn Worker Types and Configuration
Gunicorn supports several worker types, but for PHP-FPM, the most relevant is the `sync` worker type, which is the default. Each worker process handles one request at a time. The key is to configure the number of workers and their behavior to match the demands of your PHP application and the capacity of your PHP-FPM setup.
`–workers` and `–worker-connections` (Conceptual for PHP-FPM Proxy)**
When Gunicorn is acting as a proxy to PHP-FPM, the `–workers` directive in Gunicorn should be set to a value that complements the number of PHP-FPM worker processes. A common heuristic is to set Gunicorn workers to `(2 * number_of_cpu_cores) + 1`. This provides a good balance for I/O bound tasks. However, the actual bottleneck will likely be PHP-FPM itself.
Tuning PHP-FPM Workers
The real tuning happens within PHP-FPM. The `pm` (Process Manager) setting in `php-fpm.conf` (or pool configuration files like `www.conf`) is critical. The most common modes are `static`, `dynamic`, and `ondemand`.
`pm = dynamic`
This mode is often a good balance. PHP-FPM will maintain a minimum number of idle processes (`pm.min_spare_servers`) and a maximum (`pm.max_spare_servers`). It will spawn new processes as needed, up to `pm.max_children`, and kill idle ones if there are too many.
`pm = static`
In `static` mode, PHP-FPM keeps a fixed number of child processes running at all times (`pm.max_children`). This can be more performant if you have consistent high traffic, as there’s no overhead for spawning new processes. However, it can lead to resource exhaustion if `pm.max_children` is set too high.
Key PHP-FPM Directives
; /etc/php/8.1/fpm/pool.d/www.conf (example path) [www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock ; Or a TCP/IP socket like 127.0.0.1:9000 ; Process Manager settings pm = dynamic pm.max_children = 100 ; Maximum number of children that can be alive at the same time. pm.start_servers = 10 ; Number of children to start when the pool starts. pm.min_spare_servers = 5 ; Number of children to keep idle at minimum. pm.max_spare_servers = 20 ; Number of children to keep idle at maximum. pm.max_requests = 500 ; Maximum number of requests each child process should execute before respawning. ; Other important settings request_terminate_timeout = 60s ; Timeout for script execution ; rlimit_files = 1024 ; rlimit_core = 0
Tuning `pm.max_children` is crucial. A common approach is to calculate it based on available RAM: `pm.max_children = (Total RAM – RAM for OS/other services) / Average RAM per PHP-FPM process`. Monitor your server’s memory usage closely. If you see excessive swapping, `pm.max_children` is too high. If requests are being queued or timing out, it might be too low.
Gunicorn Configuration Example (Proxying to PHP-FPM)
# Example gunicorn.conf.py or command line # Command line example: # gunicorn --workers 4 --bind 0.0.0.0:8000 myapp.wsgi:application --proxy-protocol --timeout 60 # Configuration file example (gunicorn.conf.py) import multiprocessing bind = "0.0.0.0:8000" # Or your desired IP:Port workers = multiprocessing.cpu_count() * 2 + 1 # A common starting point proxy_protocol = True # If Nginx is configured with 'proxy_protocol' timeout = 60 # Corresponds to PHP-FPM's request_terminate_timeout # If you were proxying to a Python app, you'd specify 'application'. # For PHP-FPM, Gunicorn acts as a reverse proxy. Nginx is typically configured # to proxy to Gunicorn, which then proxies to PHP-FPM. # However, a more direct and common setup is Nginx -> PHP-FPM. # If Gunicorn is *required* in the chain, it would typically be Nginx -> Gunicorn -> PHP-FPM. # In this scenario, Gunicorn's role is minimal, often just passing through. # The primary tuning remains with PHP-FPM.
Important Note: The most common and performant setup for PHP applications is Nginx directly proxying to PHP-FPM. Introducing Gunicorn into the chain for a PHP application typically adds unnecessary complexity and overhead unless there’s a specific architectural reason (e.g., integrating PHP with a Python application’s request lifecycle). If you are using Gunicorn for a PHP app, ensure it’s configured correctly as a reverse proxy to your PHP-FPM socket/port.
MongoDB Performance Tuning on OVH Instances
Optimizing MongoDB performance on OVH instances involves a combination of OS-level tuning, MongoDB configuration adjustments, and schema design. OVH instances, like any cloud provider, have specific characteristics regarding CPU, RAM, and I/O that should be considered.
OS-Level Tuning for MongoDB
MongoDB is sensitive to system resource limitations. Key OS configurations include file descriptor limits and memory management.
File Descriptors
MongoDB uses file descriptors for network connections, files, and other OS resources. Insufficient file descriptor limits can lead to connection errors and performance degradation. Increase the limits for the MongoDB user.
# Check current limits ulimit -n # Edit /etc/security/limits.conf to set limits for the mongod user # Replace 'mongod' with the user running your MongoDB instance if different sudo nano /etc/security/limits.conf # Add these lines at the end of the file: * soft nofile 65536 * hard nofile 65536 mongod soft nofile 65536 mongod hard nofile 65536 # Also, edit /etc/pam.d/common-session to ensure limits are applied sudo nano /etc/pam.d/common-session # Add this line: session required pam_limits.so # Apply changes by restarting the mongod service or rebooting sudo systemctl restart mongod
Swappiness
MongoDB performs best when it can keep its working set in RAM. Excessive swapping (using disk as virtual RAM) severely degrades performance. Reduce the system’s swappiness value.
# Check current swappiness cat /proc/sys/vm/swappiness # Set swappiness to a low value (e.g., 1 or 10) temporarily sudo sysctl vm.swappiness=10 # Make the change permanent by editing /etc/sysctl.conf sudo nano /etc/sysctl.conf # Add or modify this line: vm.swappiness = 10 # Apply the sysctl configuration sudo sysctl -p
MongoDB Configuration (`mongod.conf`)
The `mongod.conf` file contains critical settings for the MongoDB server. Key parameters for performance tuning include storage engine, journaling, and cache size.
Storage Engine and WiredTiger
MongoDB 3.2+ defaults to the WiredTiger storage engine, which is generally recommended for its compression and concurrency features. Ensure you are using WiredTiger.
# /etc/mongod.conf
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger # Ensure this is set or defaults to WiredTiger
wiredTiger:
# Compression: zlib (default), snappy, or none. zlib offers good compression.
# For higher performance, consider snappy or none if RAM is abundant.
collectionConfig:
compression: zlib
indexConfig:
prefixCompression: true
# Network interfaces to bind to
net:
port: 27017
bindIp: 127.0.0.1 # Or specific IPs for remote access, e.g., 0.0.0.0 for all interfaces (use with caution and firewall)
# Logging configuration
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
logRotate: reopen
# Process management
processManagement:
fork: true
pidFilePath: /var/run/mongodb/mongod.pid
# Security settings (important for production)
# security:
# authorization: enabled
# Performance tuning
# Sharding settings (if applicable)
# sharding:
# clusterRole: configsvr
# configsvrFilePermissions:
# _id: "600"
# For WiredTiger, cache size is crucial.
# Default is 50% of RAM, which is often a good starting point.
# Adjust based on your instance's RAM and workload.
# Example: If you have 16GB RAM, you might set it to 8GB (8192MB).
# Ensure enough RAM is left for the OS and other processes.
# If you have very high RAM (e.g., 64GB+), you might dedicate more.
# For smaller instances (e.g., 4GB), you might need to be more conservative.
# If not specified, MongoDB defaults to 50% of RAM.
# storage:
# wiredTiger:
# engineConfig:
# cacheSizeGB: 8 # Example for a 16GB RAM instance
The `wiredTiger.cacheSizeGB` parameter is critical. MongoDB automatically configures this to 50% of system RAM by default. Monitor your RAM usage. If MongoDB is consuming too much RAM and causing swapping, reduce this value. Conversely, if you have ample RAM and a large working set, increasing it (while leaving sufficient RAM for the OS and other services) can improve read performance.
Journaling
Journaling (`storage.journal.enabled: true`) is enabled by default and is crucial for data durability. It ensures that operations are logged before being applied to the data files, preventing data loss in case of unexpected shutdowns. While it adds a slight overhead, the data safety it provides is essential for most production environments.
Indexing Strategies
Effective indexing is paramount for MongoDB query performance. Analyze your query patterns and create indexes that cover your most frequent and performance-critical queries. Use `explain()` to understand query execution plans.
Example: Using `explain()`
// Connect to your MongoDB instance
// db.collection.find({ field: "value" }).explain()
// Example: Find user by email
db.users.find({ email: "[email protected]" }).explain("executionStats")
/*
Sample Output Snippet:
{
"queryPlanner" : { ... },
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 1, // Should be low if indexed
"totalDocsExamined" : 1, // Should be low if indexed
"executionStages" : { ... }
},
"serverInfo" : { ... }
}
*/
Look for `totalKeysExamined` and `totalDocsExamined`. If these numbers are high relative to `nReturned`, your query is likely not using an index effectively, or no suitable index exists. Consider creating an index on the `email` field:
db.users.createIndex({ email: 1 })
Monitoring and Profiling
Continuous monitoring is key to identifying performance bottlenecks. MongoDB provides tools for profiling slow queries and monitoring server performance.
Slow Query Profiler
Enable the slow query profiler to log queries that exceed a certain threshold. This helps pinpoint inefficient operations.
// Enable profiling (set to 1 for slow queries, 2 for all queries)
db.setProfilingLevel(1, { slowms: 100 }) // Log queries slower than 100ms
// View slow queries
db.system.profile.find().pretty()
// Disable profiling
// db.setProfilingLevel(0)
Regularly review the output of `db.system.profile.find()` and use `explain()` on the identified slow queries to optimize them. Also, leverage tools like MongoDB Atlas’s performance monitoring or third-party solutions for real-time insights.