The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Redis on Linode for PHP
Nginx as a High-Performance Frontend Proxy
For a PHP application, Nginx serves as an exceptional frontend proxy and static file server. Its event-driven, asynchronous architecture makes it incredibly efficient at handling concurrent connections, offloading the heavy lifting from your application servers. We’ll focus on tuning Nginx for optimal performance, particularly its worker processes and connection handling.
Core Nginx Configuration Tuning
The primary configuration file is typically located at /etc/nginx/nginx.conf. We need to adjust the worker_processes and worker_connections directives. The optimal number of worker_processes is usually set to the number of CPU cores available on your Linode instance. This allows Nginx to fully utilize your hardware.
worker_connections defines the maximum number of simultaneous connections that each worker process can handle. A common starting point is 1024, but this can be increased significantly depending on your server’s RAM and expected load. The theoretical maximum is limited by the system’s file descriptor limit.
Let’s look at a sample nginx.conf snippet:
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; # Increased from default 1024
multi_accept on; # Allows a worker to accept multiple connections at once
}
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
include /etc/nginx/mime.types;
default_type application/octet-stream;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m; # Adjust size as needed
ssl_session_timeout 10m; # Adjust as needed
# Gzip compression
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;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Optimizing File Descriptor Limits
To support a high number of worker_connections, you must ensure your system’s file descriptor limits are adequately set. Each connection consumes a file descriptor. You can check current limits with ulimit -n. To permanently increase these limits, edit /etc/security/limits.conf.
Add the following lines to /etc/security/limits.conf:
* soft nofile 65536 * hard nofile 65536 root soft nofile 65536 root hard nofile 65536
You’ll also need to adjust the systemd service file for Nginx (if using systemd) to inherit these limits. Create or edit /etc/systemd/system/nginx.service.d/override.conf:
[Service] LimitNOFILE=65536
After making these changes, reload the systemd daemon and restart Nginx:
sudo systemctl daemon-reload sudo systemctl restart nginx
Gunicorn: The Python WSGI HTTP Server
When deploying Python web applications (like Flask or Django) on Linode, Gunicorn is a popular and robust WSGI HTTP server. It’s designed to be simple, fast, and production-ready. Proper Gunicorn configuration is crucial for handling application requests efficiently.
Gunicorn Worker Processes and Threads
Gunicorn’s performance is heavily influenced by its worker class and the number of worker processes. The default worker class is sync, which is synchronous. For I/O-bound applications, using the gevent or eventlet worker classes (which are asynchronous) can significantly improve concurrency. However, for CPU-bound tasks, the sync worker class with multiple processes is often sufficient.
The recommended number of worker processes is typically (2 * number_of_cpu_cores) + 1. This formula accounts for handling incoming requests and background tasks. If you’re using an asynchronous worker class like gevent, you might be able to get away with fewer worker processes, as each worker can handle many concurrent connections.
Here’s an example of how to start Gunicorn with optimal settings:
# Assuming you have a WSGI application object named 'application' in 'your_app.wsgi' # And you want to bind to a local socket for Nginx to connect to. # For sync workers (CPU-bound or simpler I/O) gunicorn --workers 5 --worker-class sync --bind unix:/path/to/your_app.sock --threads 2 your_app.wsgi:application # For gevent workers (I/O-bound, high concurrency) # Make sure to install gevent: pip install gevent gunicorn --workers 3 --worker-class gevent --worker-connections 1000 --bind unix:/path/to/your_app.sock your_app.wsgi:application
Explanation:
--workers 5: Sets the number of worker processes. Adjust based on your CPU cores.--worker-class sync: Uses the synchronous worker class.--worker-class gevent: Uses the asynchronous gevent worker class.--worker-connections 1000: (For gevent) Sets the maximum number of simultaneous connections per worker.--bind unix:/path/to/your_app.sock: Binds Gunicorn to a Unix domain socket. This is generally faster than TCP sockets for local communication between Nginx and Gunicorn.your_app.wsgi:application: Points to your WSGI application object.
Gunicorn Configuration File
For more complex configurations or to manage settings easily, you can use a Gunicorn configuration file (e.g., gunicorn_config.py).
# gunicorn_config.py import multiprocessing bind = "unix:/path/to/your_app.sock" workers = (multiprocessing.cpu_count() * 2) + 1 worker_class = "sync" # or "gevent" # worker_connections = 1000 # Only for async workers # enable_stdio_inheritance = True # Useful for logging to stdout/stderr # Logging configuration loglevel = "info" accesslog = "/var/log/gunicorn/access.log" errorlog = "/var/log/gunicorn/error.log" # Other settings timeout = 30 # seconds keepalive = 2 # seconds
Then, run Gunicorn with the config file:
gunicorn -c /path/to/gunicorn_config.py your_app.wsgi:application
PHP-FPM: The FastCGI Process Manager
For traditional PHP applications, PHP-FPM (FastCGI Process Manager) is the de facto standard for handling PHP requests when Nginx is the web server. It allows Nginx to communicate with PHP scripts efficiently. Tuning PHP-FPM is critical for PHP application performance.
PHP-FPM Pool Configuration
PHP-FPM’s configuration is managed through “pools.” Each pool defines a set of worker processes that handle PHP requests. The main configuration file is typically /etc/php/[version]/fpm/php-fpm.conf, and pool configurations are in /etc/php/[version]/fpm/pool.d/www.conf (or a custom pool name).
The most important directives to tune are related to process management:
pm: Process manager control. Options arestatic,dynamic, andondemand.pm.max_children: The maximum number of child processes that will be spawned.pm.start_servers: The number of child processes to start when PHP-FPM starts.pm.min_spare_servers: The minimum number of idle (spare) processes.pm.max_spare_servers: The maximum number of idle (spare) processes.pm.max_requests: The number of requests each child process will serve before respawning.
For a server with a fixed number of CPU cores and predictable load, static is often the most performant. For dynamic environments or fluctuating loads, dynamic can be more memory-efficient.
Here’s a sample www.conf for a server with 4 CPU cores, using the dynamic process manager:
[global] pid = /run/php/[version]-fpm.pid error_log = /var/log/php/[version]-fpm.log log_level = notice [www] user = www-data group = www-data listen = /run/php/[version]-fpm.sock # Or a TCP socket like 127.0.0.1:9000 listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 50 # Adjust based on RAM and expected concurrency pm.start_servers = 5 # Initial number of workers pm.min_spare_servers = 2 # Minimum idle workers pm.max_spare_servers = 10 # Maximum idle workers pm.max_requests = 500 # Restart worker after 500 requests request_terminate_timeout = 120 # seconds request_slowlog_timeout = 30 # seconds, logs requests slower than this catch_workers_output = yes # Log worker output to the main error log
Tuning pm.max_children: This is the most critical setting. A common rule of thumb is to calculate the maximum memory a PHP process can consume (e.g., 20MB for a lean app, 50MB+ for complex ones with many extensions) and divide your server’s available RAM by this value. Ensure you leave enough RAM for the OS and other services (like Nginx and Redis).
For example, if your server has 4GB RAM (4096MB) and each PHP-FPM process averages 40MB, you could theoretically have 4096MB / 40MB = 102 children. However, you need to account for Nginx, Redis, and the OS. A safer starting point might be 50-75% of this theoretical maximum.
After modifying www.conf, restart PHP-FPM:
sudo systemctl restart php[version]-fpm
Nginx Configuration for PHP-FPM
Your Nginx server block needs to be configured to pass PHP requests to PHP-FPM. Ensure you’re using the correct socket or TCP address defined in your www.conf.
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_app/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Use the socket defined in php-fpm pool
fastcgi_pass unix:/run/php/[version]-fpm.sock;
# Or use TCP: fastcgi_pass 127.0.0.1:9000;
}
# Deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
location ~ /\.ht {
deny all;
}
# Caching for static assets
location ~* \.(css|js|jpg|jpeg|gif|png|ico|svg|webp)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
Redis: In-Memory Data Structure Store
Redis is an invaluable tool for caching, session management, and message queuing. Proper configuration can drastically reduce database load and improve application responsiveness.
Redis Configuration Tuning
The main Redis configuration file is typically /etc/redis/redis.conf. Key parameters for performance include memory management and persistence.
Memory Management
maxmemory: This directive sets a hard limit on the amount of memory Redis can use. It’s crucial to prevent Redis from consuming all available RAM. Set this to a value that leaves ample room for your OS and other applications.
maxmemory-policy: This defines how Redis evicts keys when maxmemory is reached. For caching scenarios, allkeys-lru (Least Recently Used) is a common and effective choice.
# /etc/redis/redis.conf # Set a memory limit (e.g., 1GB for a server with 4GB RAM, leaving 3GB for OS/apps) maxmemory 1gb # Eviction policy for when maxmemory is reached # Options: noeviction, allkeys-lru, volatile-lru, allkeys-random, volatile-random, volatile-ttl, allkeys-lfu, volatile-lfu maxmemory-policy allkeys-lru
Persistence
Redis offers two main persistence mechanisms: RDB snapshots and AOF (Append Only File). For high-performance caching, you might consider disabling or reducing the frequency of persistence if data loss on restart is acceptable. If data durability is critical, tune these carefully.
save directives (RDB): These define intervals at which Redis will save the dataset to disk. For a cache, you might comment these out or set them to very infrequent intervals.
appendonly no: Setting this to no disables AOF persistence. If you need AOF, consider tuning appendfsync (e.g., appendfsync everysec is a good balance).
# /etc/redis/redis.conf # RDB persistence (comment out or adjust for caching) # save 900 1 # save 300 10 # save 60 10000 # AOF persistence (set to 'no' for pure cache, or tune) appendonly no # appendfsync everysec # If appendonly is 'yes'
Network and Connection Tuning
tcp-backlog: Controls the queue size for pending connections. Increasing this can help during high connection spikes.
maxclients: The maximum number of clients that can be connected simultaneously. Ensure this is higher than your application’s expected concurrent Redis connections.
# /etc/redis/redis.conf tcp-backlog 511 # Default is 511, can be increased if needed, e.g., 1024 # Max clients (default is 10000) maxclients 20000
After modifying redis.conf, restart the Redis service:
sudo systemctl restart redis-server
Putting It All Together: Linode Deployment Strategy
On a Linode instance, you’ll typically deploy these components together. A common setup involves:
- Nginx: Running as the public-facing web server, handling SSL termination, serving static assets, and proxying dynamic requests to Gunicorn/PHP-FPM.
- Gunicorn/PHP-FPM: Running as the application server, listening on a Unix domain socket or localhost TCP port.
- Redis: Running as a separate service, accessible via its network interface (usually localhost).
Ensure your firewall (e.g., ufw) is configured to allow necessary traffic (typically port 80 and 443 for Nginx). The communication between Nginx and Gunicorn/PHP-FPM should ideally be over Unix domain sockets for performance, as they avoid the overhead of TCP/IP stack processing.
Regular monitoring of CPU, memory, network I/O, and Redis memory usage is essential. Tools like htop, iotop, redis-cli --stat, and application-specific performance monitoring (APM) solutions will help you identify bottlenecks and further refine these configurations.