• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on Linode for Python

The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on Linode for Python

Nginx as a High-Performance Frontend Proxy

For Python web applications, Nginx serves as an exceptional frontend proxy, efficiently handling static file serving, SSL termination, and request routing to your application server (Gunicorn for WSGI or PHP-FPM for PHP). Optimizing Nginx is crucial for maximizing throughput and minimizing latency.

A foundational Nginx configuration for a Python application using Gunicorn might look like this. We’ll focus on key directives that impact performance.

Nginx Configuration Tuning

Edit your Nginx site configuration file, typically located at /etc/nginx/sites-available/your_app. Ensure you have a server block that proxies requests to your Gunicorn instance.

# /etc/nginx/sites-available/your_app

user www-data;
worker_processes auto; # Set to the number of CPU cores
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024; # Adjust based on expected concurrent connections
    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;

    # MIME types
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # SSL configuration (if applicable)
    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
    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
    access_log /var/log/nginx/your_app.access.log;
    error_log /var/log/nginx/your_app.error.log;

    # Static file caching
    location /static/ {
        alias /path/to/your/app/static/;
        expires 30d;
        add_header Cache-Control "public";
        access_log off;
    }

    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;
        proxy_read_timeout 300s; # Increase if your app has long-running requests
        proxy_connect_timeout 75s;
    }
}

Key Directives Explained:

  • worker_processes auto;: Dynamically sets the number of worker processes to match your CPU cores. This is generally optimal.
  • worker_connections: Defines the maximum number of simultaneous connections a worker process can handle. Tune this based on your server’s RAM and expected load. A common starting point is 1024.
  • sendfile on;: Enables efficient transfer of files from disk to network socket without user-space buffering.
  • tcp_nopush on;: Instructs Nginx to send headers in one packet after data is received from the application server.
  • tcp_nodelay on;: Disables the Nagle algorithm, which can reduce latency by sending small packets immediately.
  • keepalive_timeout: Sets the timeout for persistent connections. A moderate value (e.g., 65 seconds) balances resource usage with connection efficiency.
  • gzip_* directives: Enable and configure Gzip compression for text-based responses, significantly reducing bandwidth and improving load times.
  • ssl_* directives: Crucial for secure connections. Tuning session cache and timeouts can improve SSL handshake performance for returning clients.
  • location /static/: Configures Nginx to serve static assets directly, bypassing the application server. expires and Cache-Control headers enable browser caching.
  • proxy_pass: Specifies the upstream server. This can be a Unix socket (faster, for local communication) or an IP:port.
  • proxy_set_header: Forwards essential client information to the backend application.
  • proxy_read_timeout, proxy_connect_timeout: These are critical for preventing Nginx from dropping connections to a slow backend. Increase them if your application performs long-running tasks.

After modifying the configuration, test it with sudo nginx -t and reload Nginx with sudo systemctl reload nginx.

Gunicorn: The WSGI HTTP Server

Gunicorn (Green Unicorn) is a popular WSGI HTTP Server for Python. Its performance is heavily influenced by its worker count and type.

For most Python applications, especially those with I/O-bound tasks (like database queries or external API calls), the gevent worker type offers excellent concurrency. If your application is CPU-bound, the default sync worker or threads worker might be more appropriate, though gevent can still be beneficial if you have many short-lived I/O waits.

Gunicorn Worker Tuning

A common command-line invocation or systemd service file configuration for Gunicorn:

# Example systemd service file: /etc/systemd/system/gunicorn.service

[Unit]
Description=Gunicorn instance to serve your_app
After=network.target

[Service]
User=your_user
Group=www-data
WorkingDirectory=/path/to/your/app
ExecStart=/path/to/your/venv/bin/gunicorn \
    --workers 3 \
    --worker-class gevent \
    --bind unix:/run/gunicorn.sock \
    --log-level info \
    --access-logfile /var/log/gunicorn/your_app.access.log \
    --error-logfile /var/log/gunicorn/your_app.error.log \
    your_app.wsgi:application

[Install]
# Start Gunicorn when the system boots up
WantedBy=multi-user.target

Tuning Gunicorn Workers:

  • --workers N: The number of worker processes. A common recommendation is (2 * number_of_cores) + 1. However, for gevent workers, you might need more workers as they are cooperative multitasking. Start with 3 or 5 and monitor.
  • --worker-class gevent: Utilizes gevent for asynchronous I/O. Requires gevent to be installed (`pip install gevent`).
  • --bind unix:/run/gunicorn.sock: Binds Gunicorn to a Unix socket. This is generally faster than binding to an IP address and port for local communication between Nginx and Gunicorn. Ensure the user running Nginx (e.g., www-data) has permissions to access this socket.
  • --log-level and log files: Essential for debugging and monitoring.

To start and manage Gunicorn with systemd:

sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl status gunicorn
sudo systemctl restart gunicorn

PHP-FPM for PHP Applications

If your application is PHP-based, PHP-FPM (FastCGI Process Manager) is the standard way to interface PHP with web servers like Nginx.

PHP-FPM Configuration Tuning

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., 8.1).

; /etc/php/X.Y/fpm/pool.d/www.conf

[www]
user = www-data
group = www-data
listen = /run/php/phpX.Y-fpm.sock ; Or 127.0.0.1:9000
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 10
pm.process_idle_timeout = 10s

request_terminate_timeout = 300
request_slowlog_timeout = 30
slowlog = /var/log/php/php-fpm/www-slow.log

catch_workers_output = yes
; If you are using PHP 7.4 or older, use:
; catch_workers_output = 1
; If you are using PHP 8.0 or newer, use:
; catch_workers_output = true

; For PHP 8.0+
; pm.enable_dynamic_memory_limit = true
; pm.memory_limit = 256M ; Example, adjust as needed

; For older PHP versions, you might set memory_limit globally or per script
; memory_limit = 256M

Key PHP-FPM Directives:

  • listen: The address and port or Unix socket PHP-FPM listens on. Unix sockets are preferred for local communication.
  • pm: Process Manager control. dynamic is common, allowing FPM to adjust the number of child processes based on load. Other options include static (fixed number of children) and ondemand.
  • pm.max_children: The maximum number of child processes that will be created. This is the most critical setting. Too high can exhaust RAM; too low can lead to request queuing. A good starting point is (total_RAM_MB / average_process_size_MB), but monitor usage.
  • pm.start_servers: The number of child processes started when FPM starts.
  • pm.min_spare_servers: The minimum number of idle supervisor processes.
  • pm.max_spare_servers: The maximum number of idle supervisor processes.
  • pm.process_idle_timeout: The number of seconds after which an idle process will be killed.
  • request_terminate_timeout: Maximum execution time for a single script.
  • request_slowlog_timeout: If a script runs longer than this, it’s logged in the slow log.
  • catch_workers_output: Captures stdout and stderr from worker processes. Useful for debugging.

After changes, restart PHP-FPM:

sudo systemctl restart phpX.Y-fpm

PostgreSQL Performance Tuning

PostgreSQL’s performance is heavily dependent on its configuration, particularly memory allocation and query optimization. On Linode, you’ll be managing a single PostgreSQL instance.

PostgreSQL Configuration Tuning (postgresql.conf)

The main configuration file is postgresql.conf, typically found in /etc/postgresql/X.Y/main/. Always back up this file before making changes.

# postgresql.conf - Example tuning parameters

# Shared Memory
shared_buffers = 25% of total RAM  # e.g., 2GB for 8GB RAM
# Example: shared_buffers = 2GB

# Checkpointing
max_wal_size = 4GB  # Adjust based on write activity and disk space
min_wal_size = 1GB  # Keep a minimum for recovery
checkpoint_completion_target = 0.9 # Spread checkpoints over time

# Memory Usage
work_mem = 16MB # Per sort operation. Adjust based on complex queries and RAM. Start low.
maintenance_work_mem = 256MB # For VACUUM, CREATE INDEX, etc.

# Connections
max_connections = 100 # Adjust based on application needs and server resources
shared_buffers is the most important parameter.
effective_cache_size = 50% of total RAM # Tells the planner how much OS cache is available.

# Autovacuum
autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = 15s
autovacuum_vacuum_threshold = 50
autovacuum_analyze_threshold = 50

# Logging
log_destination = 'stderr'
logging_collector = on
log_directory = 'pg_log'
log_filename = 'postgresql-%Y-%m-%d_%H-%M-%S.log'
log_statement = 'ddl' # Log Data Definition Language statements
log_min_duration_statement = 250ms # Log queries longer than 250ms
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
log_temp_files = 0 # Log temp files larger than 0 KB

Key PostgreSQL Directives:

  • shared_buffers: The most critical parameter. This is the memory PostgreSQL uses for caching data. A common recommendation is 25% of total system RAM. Do not set it too high, as the OS also needs RAM.
  • max_wal_size and min_wal_size: Control the size of Write-Ahead Log (WAL) files. Larger values reduce checkpoint frequency but increase recovery time. Tune based on your write load.
  • checkpoint_completion_target: Spreads checkpoint I/O over time, reducing I/O spikes.
  • work_mem: Memory used for internal sort operations and hash tables. Crucial for complex queries (ORDER BY, DISTINCT, JOINs). Set this cautiously; it’s allocated per sort/hash operation within a query. Too high can lead to out-of-memory errors.
  • maintenance_work_mem: Memory used for maintenance operations like VACUUM, CREATE INDEX, and ALTER TABLE ADD FOREIGN KEY. Larger values speed up these operations.
  • max_connections: The maximum number of concurrent client connections. Ensure this is sufficient for your application but not so high that it exhausts server resources.
  • effective_cache_size: Informs the query planner about the total memory available for caching (PostgreSQL’s shared_buffers + OS file system cache). Setting this to 50-75% of total RAM is typical.
  • autovacuum settings: Essential for reclaiming space from dead tuples and preventing transaction ID wraparound. Tune autovacuum_max_workers and thresholds based on your table activity.
  • Logging directives: Crucial for identifying slow queries and troubleshooting. log_min_duration_statement is invaluable for finding performance bottlenecks.

After modifying postgresql.conf, you need to restart PostgreSQL:

sudo systemctl restart postgresql

PostgreSQL Query Optimization and Indexing

Tuning PostgreSQL goes beyond configuration. Efficient queries and proper indexing are paramount.

Using EXPLAIN ANALYZE

The EXPLAIN ANALYZE command is your best friend for understanding query execution plans.

EXPLAIN ANALYZE
SELECT
    users.id,
    users.username,
    COUNT(posts.id) AS post_count
FROM
    users
LEFT JOIN
    posts ON users.id = posts.user_id
WHERE
    users.created_at > '2023-01-01'
GROUP BY
    users.id, users.username
ORDER BY
    post_count DESC
LIMIT 10;

Look for:

  • Sequential Scans (Seq Scan) on large tables where an index could be used.
  • High costs associated with certain operations.
  • Large row counts being processed compared to the final output.
  • Nested Loop joins when a Hash Join or Merge Join would be more efficient.
  • Sorts that could be avoided with an index.

Effective Indexing Strategies

Create indexes on columns used in WHERE clauses, JOIN conditions, and ORDER BY clauses. Consider multi-column indexes.

-- Index for the WHERE clause
CREATE INDEX idx_users_created_at ON users (created_at);

-- Index for the JOIN condition
CREATE INDEX idx_posts_user_id ON posts (user_id);

-- Multi-column index for WHERE and ORDER BY (if applicable)
-- CREATE INDEX idx_users_created_at_username ON users (created_at, username);

-- Index for the GROUP BY clause (often covered by JOIN or WHERE indexes)
-- CREATE INDEX idx_posts_user_id_for_group ON posts (user_id);

Partial Indexes: Useful when you frequently query a subset of data.

-- Example: Index for active users only
CREATE INDEX idx_users_active ON users (id) WHERE is_active = TRUE;

Expression Indexes: Indexing the result of a function or expression.

-- Example: Indexing a lowercased column for case-insensitive searches
CREATE INDEX idx_users_email_lower ON users (lower(email));

Monitoring and Iteration

Performance tuning is an iterative process. Regularly monitor your system’s resource utilization (CPU, RAM, I/O, network) using tools like htop, iotop, and Linode’s dashboard. Analyze Nginx, Gunicorn/PHP-FPM, and PostgreSQL logs for errors and slow operations. Use application performance monitoring (APM) tools if available. Make incremental changes and measure their impact.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing indexing lock conflicts and high CPU during bulk stock updates on DigitalOcean Servers
  • How to Debug and Fix memory leaks and socket exhaustion in daemon processes in Modern C++ Applications
  • Infrastructure as Code: Provisioning Secure PHP Clusters on DigitalOcean Using Terraform
  • Fixing Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Legacy Laravel Codebases Without Breaking API Contracts
  • An Auditor’s Checklist for Securing Laravel Backends on Google Cloud

Copyright © 2026 · Vinay Vengala