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

Vengala Vinay

Having 12+ 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 PHP

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 when pm is set to dynamic or the fixed number when set to static. This is the most critical setting.
  • pm.start_servers: The number of child processes to start when FPM starts (for dynamic).
  • pm.min_spare_servers: The minimum number of idle spare servers (for dynamic).
  • pm.max_spare_servers: The maximum number of idle spare servers (for dynamic).

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 like VACUUM, CREATE INDEX, and ALTER 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 -1 or a value like 16MB can improve write performance.
  • max_worker_processes: The maximum number of background processes that can be started. Important for parallel query execution.
  • max_parallel_workers and max_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-status module, htop, netdata.
  • PHP-FPM: php-fpm-status page, htop, netdata.
  • Gunicorn: htop, netdata, application-specific monitoring.
  • PostgreSQL: pg_stat_activity, pg_stat_statements extension, 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.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability
  • Scala Pekko vs. Go Goroutines: Actor Model vs. CSP for Event-Driven Reactive Systems
  • Java Loom Virtual Threads vs. Go Goroutines: Under-the-Hood Scheduler and Thread Overhead Comparison

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (584)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (806)
  • PHP (5)
  • PHP Development (21)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (19)
  • Ruby on Rails (1)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (7)
  • WordPress Theme Development (357)

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (806)
  • Debugging & Troubleshooting (584)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala