• 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 DigitalOcean for C

The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on DigitalOcean for C

Nginx as a High-Performance Frontend Proxy

For serving dynamic web applications, Nginx excels as a reverse proxy, efficiently handling client connections and forwarding requests to your application server (Gunicorn for Python, PHP-FPM for PHP). Optimizing Nginx is crucial for minimizing latency and maximizing throughput.

Core Nginx Configuration Tuning

The primary configuration file, typically located at /etc/nginx/nginx.conf, contains directives that govern worker processes, connection handling, and caching. Start by adjusting the worker_processes directive. A common best practice is to set it to the number of CPU cores available on your server. For a DigitalOcean droplet with 4 vCPUs, this would be:

worker_processes 4; # Or auto

The worker_connections directive defines the maximum number of simultaneous connections that each worker process can handle. This should be set high enough to accommodate your expected traffic, considering that each connection consumes a file descriptor. A typical starting point is 1024 or higher, but this is often limited by the system’s open file descriptor limit. Ensure your system’s limits are also adjusted.

events {
    worker_connections 4096; # Adjust based on system limits and expected load
    multi_accept on;
}

Next, configure your server block (virtual host) for your application. This involves setting up the proxy pass to your application server and enabling essential features like keepalive connections and Gzip compression.

http {
    # ... other http directives ...

    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;

    # Gzip Compression
    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;

    server {
        listen 80;
        server_name your_domain.com;

        location / {
            proxy_pass http://unix:/path/to/your/app.sock; # For Gunicorn/uWSGI
            # OR
            # proxy_pass http://127.0.0.1:8000; # For Gunicorn on a TCP port
            # OR
            # proxy_pass http://127.0.0.1:9000; # For PHP-FPM

            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_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;

            # For WebSocket support (if applicable)
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }

        # Serve static files directly from Nginx for better performance
        location /static/ {
            alias /path/to/your/static/files/;
            expires 30d;
            add_header Cache-Control "public";
        }

        location /media/ {
            alias /path/to/your/media/files/;
            expires 30d;
            add_header Cache-Control "public";
        }
    }

    # ... other http directives ...
}

System-Level File Descriptor Limits

Nginx’s ability to handle many connections is often bottlenecked by the operating system’s limit on open file descriptors. You need to increase these limits. Edit /etc/security/limits.conf:

# Increase limits for the nginx user (or the user your nginx runs as)
nginx soft nofile 65536
nginx hard nofile 65536

# For all users (if nginx user is not explicitly defined)
* soft nofile 65536
* hard nofile 65536

You also need to configure systemd to increase the limits for the Nginx service if you’re using systemd to manage it (which is standard on modern DigitalOcean images). Create or edit a file like /etc/systemd/system/nginx.service.d/limits.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

Gunicorn is a popular WSGI HTTP Server for Python. Its performance is heavily influenced by the number of worker processes and the worker type. For most CPU-bound applications, the sync worker class is common, but for I/O-bound tasks, gevent or eventlet can offer better concurrency.

Gunicorn Worker Configuration

The recommended number of worker processes is typically (2 * number_of_cores) + 1. This formula aims to keep CPU cores busy while accounting for potential I/O waits. For a 4-core DigitalOcean droplet, this would be 9 workers.

# Example command-line invocation
gunicorn --workers 9 --worker-class sync --bind unix:/path/to/your/app.sock your_project.wsgi:application

# Or binding to a TCP port
gunicorn --workers 9 --worker-class sync --bind 127.0.0.1:8000 your_project.wsgi:application

If your application is heavily I/O bound (e.g., making many external API calls or database queries that don’t block the CPU), consider using an asynchronous worker class like gevent. This requires installing the gevent library (`pip install gevent`).

# Example with gevent workers
gunicorn --workers 9 --worker-class gevent --worker-connections 1000 --bind unix:/path/to/your/app.sock your_project.wsgi:application

The --worker-connections directive is relevant for asynchronous workers and specifies the maximum number of simultaneous connections each worker can handle. Tune this based on your application’s I/O patterns.

Gunicorn Timeout and Keepalive

The --timeout setting determines how long Gunicorn will wait for a worker to process a request before considering it timed out. Setting this too low can lead to premature errors for long-running requests, while setting it too high can tie up workers unnecessarily. A value between 30 and 120 seconds is common.

gunicorn --workers 9 --timeout 120 --bind unix:/path/to/your/app.sock your_project.wsgi:application

Gunicorn’s keepalive settings are less critical when Nginx is handling the client connections and proxying. Nginx’s keepalive_timeout is more relevant for client-to-Nginx connections. Ensure Nginx’s proxy timeouts are generous enough to not prematurely cut off requests that Gunicorn might be processing.

PHP-FPM: FastCGI Process Manager for PHP

For PHP applications, PHP-FPM is the standard FastCGI process manager. Its performance is governed by the number of child processes it manages and how they are spawned.

PHP-FPM Pool Configuration

PHP-FPM pools are configured in files typically found in /etc/php/X.Y/fpm/pool.d/www.conf (replace X.Y with your PHP version). The key directives are related to process management.

The pm (process manager) can be set to static, dynamic, or ondemand. For predictable performance and to avoid the overhead of spawning/killing processes, static is often preferred on dedicated servers.

; For a 4-core server, you might set a fixed number of children
pm = static
pm.max_children = 50 ; Adjust based on memory and expected load
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.process_idle_timeout = 10s

If using dynamic, you’ll configure pm.max_children, pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers. The goal is to have enough processes to handle peak load without exhausting server memory.

; Example with dynamic process management
pm = dynamic
pm.max_children = 100 ; Max number of children that can be started
pm.start_servers = 5    ; Number of children when FPM starts
pm.min_spare_servers = 5 ; Min number of idle resp. to keep
pm.max_spare_servers = 15 ; Max number of idle resp. to keep
pm.max_requests = 500   ; Max requests a child process will serve before respawning

pm.max_requests is important for preventing memory leaks in long-running PHP scripts or extensions. Setting it to a reasonable number (e.g., 500-1000) ensures processes are recycled.

PHP-FPM Request Timeout

The request_terminate_timeout directive in the PHP-FPM pool configuration controls how long a single script can run before being killed. This is crucial for preventing runaway scripts from consuming resources indefinitely. It should generally align with or be slightly longer than your application’s expected maximum script execution time, and also consider Nginx’s proxy timeouts.

request_terminate_timeout = 120s

After modifying PHP-FPM configuration, reload the service:

sudo systemctl reload phpX.Y-fpm

PostgreSQL Performance Tuning

Database performance is often the ultimate bottleneck. PostgreSQL offers extensive tuning parameters, primarily controlled by postgresql.conf. The key is to balance resource utilization (CPU, RAM, I/O) with query performance.

Key `postgresql.conf` Directives

Locate your postgresql.conf file (often in /etc/postgresql/X.Y/main/ or similar). The following parameters are critical:

  • shared_buffers: This is the most important parameter. It defines the amount of memory PostgreSQL uses for caching data. A common recommendation is 25% of your system’s RAM, but avoid exceeding 40% to leave enough memory for the OS and other processes. For a 4GB RAM droplet, 1GB (1024MB) is a good starting point.
  • work_mem: Memory used for internal sort operations and hash tables. If this is too low, sorts will spill to disk, drastically slowing down queries. Set it based on your query complexity and available RAM. Start with 16MB and increase if you see disk-based sorts in EXPLAIN ANALYZE output.
  • maintenance_work_mem: Memory used for maintenance operations like VACUUM, CREATE INDEX, and ALTER TABLE. A larger value can significantly speed up these operations. 128MB or 256MB is common for dedicated DB servers.
  • effective_cache_size: This tells PostgreSQL how much memory is available for disk caching by both PostgreSQL itself (shared_buffers) and the operating system. A good estimate is 50-75% of total RAM. This helps the query planner make better decisions.
  • wal_buffers: Memory used for WAL (Write-Ahead Logging) data. A value of -1 (auto) is often fine, but a small increase to 16MB can sometimes help.
  • max_connections: The maximum number of concurrent connections. This should be set based on your application’s needs and server resources. Too many connections can exhaust memory.
  • checkpoint_completion_target: Controls how far apart checkpoints are spread. A value of 0.9 means checkpoints are spread over 90% of the interval between checkpoints, reducing I/O spikes.
  • random_page_cost: PostgreSQL estimates the cost of fetching a random page from disk. The default is 4.0, assuming slow HDDs. For SSDs (common on DigitalOcean), reducing this to 1.1 or 1.5 can lead to better query plans.
# Example postgresql.conf settings for a 4GB RAM DigitalOcean Droplet
shared_buffers = 1024MB
work_mem = 32MB
maintenance_work_mem = 256MB
effective_cache_size = 3072MB # ~75% of 4GB RAM
wal_buffers = 16MB
max_connections = 100
checkpoint_completion_target = 0.9
random_page_cost = 1.5

After modifying postgresql.conf, you must restart the PostgreSQL service for the changes to take effect:

sudo systemctl restart postgresql

Connection Pooling with PgBouncer

For applications with many short-lived database connections (e.g., web applications), maintaining a large number of direct PostgreSQL connections can be resource-intensive. PgBouncer is a lightweight connection pooler that significantly reduces this overhead. It sits between your application and PostgreSQL, managing a pool of persistent connections to the database and handing them out to clients as needed.

Install PgBouncer:

sudo apt update
sudo apt install pgbouncer

Configure PgBouncer, typically in /etc/pgbouncer/pgbouncer.ini. The [databases] section defines how to connect to your PostgreSQL instance, and the [pgbouncer] section controls pooler behavior.

[databases]
mydatabase = host=127.0.0.1 port=5432 dbname=your_db_name user=your_db_user password=your_db_password

[pgbouncer]
listen_addr = 127.0.0.1  ; Or 0.0.0.0 to listen on all interfaces
listen_port = 6432
auth_type = md5          ; Or scram-sha-256, trust, etc.
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = session      ; 'session' is generally recommended for web apps
max_client_conn = 1000   ; Max clients connecting to pgbouncer
default_pool_size = 20   ; Pool size per database
min_pool_size = 5        ; Min pool size per database
pool_timeout = 5         ; Seconds to wait for a connection in the pool
server_reset_query = DISCARD ALL ; Recommended for session pooling
log_connections = 0
log_disconnections = 0

Create the userlist.txt file with credentials for PgBouncer to authenticate with PostgreSQL. The format is "username" "password".

"your_db_user" "your_db_password"

Ensure the pgbouncer user has appropriate permissions in PostgreSQL. You’ll also need to configure PostgreSQL to allow connections from PgBouncer’s IP and port (e.g., by editing pg_hba.conf).

# Example pg_hba.conf entry (adjust IP/method as needed)
host    your_db_name    your_db_user    127.0.0.1/32    md5

Start and enable PgBouncer, then restart PostgreSQL and PgBouncer:

sudo systemctl start pgbouncer
sudo systemctl enable pgbouncer
sudo systemctl restart postgresql
sudo systemctl restart pgbouncer

Finally, update your application’s database connection settings to point to PgBouncer’s host and port (e.g., 127.0.0.1:6432) instead of directly to PostgreSQL.

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