The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on Linode for PHP
Nginx as a High-Performance Frontend Proxy
For a PHP application, Nginx serves as an exceptional frontend proxy, efficiently handling static assets and buffering incoming requests before forwarding them to your application server (Gunicorn for Python/WSGI, or PHP-FPM for PHP). Optimizing Nginx is crucial for maximizing throughput and minimizing latency. We’ll focus on key directives that impact performance.
Nginx Worker Processes and Connections
The worker_processes directive dictates how many worker processes Nginx will spawn. Setting this to auto is generally recommended, allowing Nginx to detect the number of CPU cores and adjust accordingly. The worker_connections directive sets the maximum number of simultaneous connections that each worker process can handle. A common starting point is 1024, but this can be increased based on your server’s RAM and expected load.
Tuning Nginx Configuration
Edit your main Nginx configuration file, typically located at /etc/nginx/nginx.conf. Ensure the following directives are set appropriately within the http block:
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Increased from default 1024
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
# Gzip compression for text-based assets
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;
# Buffering and timeouts for upstream connections
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Include other configuration files
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load balancing (if using multiple application servers)
# upstream php-app {
# server 127.0.0.1:9000;
# server 127.0.0.1:9001;
# }
# Server block example for PHP-FPM
server {
listen 80;
server_name your_domain.com www.your_domain.com;
root /var/www/your_app;
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 correct socket or IP:Port for your PHP-FPM pool
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
# fastcgi_pass 127.0.0.1:9000;
}
# Deny access to hidden files
location ~ /\.ht {
deny all;
}
# Caching for static assets (adjust cache duration as needed)
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
# Server block example for Gunicorn (WSGI)
# server {
# listen 80;
# server_name your_domain.com www.your_domain.com;
#
# location / {
# proxy_pass http://127.0.0.1:8000; # Assuming Gunicorn is listening on 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;
# }
#
# location /static/ { # Serve static files directly
# alias /path/to/your/app/static/;
# expires 30d;
# add_header Cache-Control "public, no-transform";
# }
# }
}
After making changes, test your Nginx configuration and reload the service:
sudo nginx -t sudo systemctl reload nginx
PHP-FPM Tuning for PHP Applications
PHP-FPM (FastCGI Process Manager) is the de facto standard for running PHP applications with Nginx. Its performance is heavily influenced by its process management settings. The primary configuration file is typically /etc/php/X.Y/fpm/php-fpm.conf, and pool configurations are in /etc/php/X.Y/fpm/pool.d/www.conf (replace X.Y with your PHP version, e.g., 7.4).
PHP-FPM Process Manager Settings
The pm directive controls how PHP-FPM manages worker processes. The most common and recommended settings are:
static: A fixed number of child processes are spawned when FPM starts and remain active. Best for predictable high-load environments where you want minimal latency.dynamic: FPM spawns processes as needed, up to a defined maximum, and can kill idle processes to save resources. Good for variable loads.ondemand: Processes are only spawned when a request is received and killed after a period of inactivity. Saves memory but can introduce higher latency for the first request.
For dynamic and static, you’ll configure:
pm.max_children: The maximum number of child processes that will be created whenpmis set todynamicor the fixed number when set tostatic. This is the most critical setting.pm.start_servers: The number of child processes to start when FPM starts (fordynamic).pm.min_spare_servers: The minimum number of idle spare servers (fordynamic).pm.max_spare_servers: The maximum number of idle spare servers (fordynamic).
A common starting point for tuning pm.max_children is to calculate based on available RAM. Each PHP-FPM worker can consume a certain amount of memory. Estimate this (e.g., 20-50MB per process) and divide your total available RAM by this figure, then subtract memory used by the OS, Nginx, and PostgreSQL. A safe bet is often to set it to (total_ram_in_MB - reserved_for_OS_and_DB) / memory_per_process_in_MB.
Tuning PHP-FPM Pool Configuration
Edit your pool configuration file (e.g., /etc/php/7.4/fpm/pool.d/www.conf):
; /etc/php/7.4/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /var/run/php/php7.4-fpm.sock ; Or use TCP/IP: listen = 127.0.0.1:9000 listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 100 ; Adjust based on server RAM and expected load pm.start_servers = 10 ; For dynamic PM pm.min_spare_servers = 5 ; For dynamic PM pm.max_spare_servers = 20 ; For dynamic PM pm.process_idle_timeout = 10s ; Kill idle processes after 10 seconds ; Request termination after N requests ; pm.max_requests = 500 ; Set to 'on' if you want to use OPcache opcache.enable=1 opcache.memory_consumption=128 ; Adjust based on your application's needs opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.revalidate_freq=60 opcache.save_comments=1 opcache.enable_cli=1 ; Other useful settings request_terminate_timeout = 60s ; Timeout for script execution ; rlimit_files = 4096 ; rlimit_core = 0
After modifying the PHP-FPM configuration, test and reload the service:
sudo systemctl restart php7.4-fpm
Gunicorn Tuning for Python/WSGI Applications
If you’re running a Python web application using a WSGI server like Gunicorn, tuning is essential. Gunicorn’s performance is primarily determined by its worker class and the number of worker processes.
Gunicorn Worker Classes and Count
Gunicorn offers several worker classes:
sync: The default worker class. It handles one request at a time per worker. Simple but can be a bottleneck under high concurrency.eventlet,gevent: Asynchronous worker classes that use green threads to handle multiple requests concurrently. These are generally preferred for I/O-bound applications.gthread: Uses a thread pool to handle requests.
The number of workers is typically set using the -w or --workers flag. A common recommendation is (2 * number_of_cpu_cores) + 1. However, for asynchronous workers (gevent/eventlet), you might need fewer workers as each worker can handle many connections.
Gunicorn Command Line Configuration
Here’s an example of how you might start Gunicorn, often managed by systemd:
# Example systemd service file for Gunicorn
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn instance to serve myapp
After=network.target
[Service]
User=your_user
Group=your_group
WorkingDirectory=/path/to/your/app
ExecStart=/path/to/your/venv/bin/gunicorn \
--workers 4 \
--worker-class gevent \
--bind 127.0.0.1:8000 \
--timeout 120 \
myapp.wsgi:application
[Install]
WantedBy=multi-user.target
In this example:
--workers 4: Sets the number of worker processes. Adjust this based on your CPU cores and worker class.--worker-class gevent: Uses the gevent worker class for concurrency.--bind 127.0.0.1:8000: Binds Gunicorn to a local address and port. Nginx will proxy to this.--timeout 120: Sets the worker timeout to 120 seconds.
After creating or modifying the service file, reload systemd and start/restart Gunicorn:
sudo systemctl daemon-reload sudo systemctl restart gunicorn sudo systemctl status gunicorn
PostgreSQL Performance Tuning
A slow database can be the ultimate bottleneck. PostgreSQL offers extensive tuning parameters, primarily controlled by postgresql.conf. The location varies by distribution but is often found in /etc/postgresql/X.Y/main/postgresql.conf.
Key PostgreSQL Configuration Parameters
Here are some of the most impactful parameters for performance:
shared_buffers: The amount of memory dedicated to PostgreSQL’s shared memory buffers. A common recommendation is 25% of system RAM, but not exceeding 8GB on systems with less than 32GB RAM. On systems with ample RAM, it can go higher.work_mem: The maximum amount of memory that can be used for internal sort operations and hash tables before spilling to disk. Crucial for complex queries. Set it per-query, not per-connection.maintenance_work_mem: The maximum memory to be used for maintenance operations likeVACUUM,CREATE INDEX, andALTER TABLE ADD FOREIGN KEY.effective_cache_size: An estimate of how much memory is available for disk caching by the operating system and PostgreSQL’s shared buffers. Helps the query planner make better decisions.wal_buffers: Memory for Write-Ahead Log (WAL) data. A value of -1 (auto) is usually fine, but setting it to-1or a value like16MBcan improve write performance.max_worker_processes: The maximum number of background processes that can be started. Important for parallel query execution.max_parallel_workersandmax_parallel_workers_per_gather: Control parallel query execution.
Tuning PostgreSQL Configuration
Edit your postgresql.conf file. Use a tool like pgtune or consult PostgreSQL documentation for precise calculations based on your server’s RAM and workload. Here’s an example snippet:
# /etc/postgresql/12/main/postgresql.conf # Memory settings shared_buffers = 2GB ; Adjust based on RAM (e.g., 25% of total RAM) work_mem = 32MB ; Adjust based on query complexity and RAM maintenance_work_mem = 512MB ; For VACUUM, CREATE INDEX etc. effective_cache_size = 6GB ; Estimate of OS + shared_buffers cache # WAL settings wal_buffers = 16MB wal_writer_delay = 200ms ; Default is 200ms commit_delay = 10ms ; For synchronous commits synchronous_commit = on ; Or 'local' for higher performance if durability is less critical # Connection settings max_connections = 100 ; Adjust based on application needs and server resources superuser_reserved_connections = 3 ; Reserve connections for superusers # Background processes max_worker_processes = 8 ; Should be at least 2 * number of CPU cores max_parallel_workers = 4 ; Number of workers for parallel queries max_parallel_workers_per_gather = 2 ; Max workers per gather node # Logging log_min_duration_statement = 250ms ; Log queries taking longer than 250ms log_statement = 'ddl' ; Log DDL statements log_directory = 'pg_log' log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' log_rotation_age = 1d log_rotation_size = 0
After modifying postgresql.conf, you must restart the PostgreSQL service:
sudo systemctl restart postgresql
Monitoring and Iteration
Tuning is an iterative process. Continuously monitor your system’s performance using tools like:
- Nginx:
nginx-statusmodule,htop,netdata. - PHP-FPM:
php-fpm-statuspage,htop,netdata. - Gunicorn:
htop,netdata, application-specific monitoring. - PostgreSQL:
pg_stat_activity,pg_stat_statementsextension,EXPLAIN ANALYZE,pg_top,netdata. - System-wide:
htop,vmstat,iostat,netdata.
Analyze logs for errors and slow operations. Make incremental changes to your configurations, test the impact, and repeat. The optimal settings will depend heavily on your specific application’s workload, traffic patterns, and server resources.