• 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 Laravel

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

Nginx as a High-Performance Frontend for Laravel

When deploying Laravel applications, Nginx serves as an exceptional choice for a web server and reverse proxy. Its event-driven, asynchronous architecture makes it highly efficient at handling concurrent connections, a critical factor for web applications. We’ll focus on tuning Nginx for optimal performance, particularly in serving static assets and proxying requests to your PHP-FPM or Gunicorn process.

Core Nginx Configuration Tuning

The primary configuration file for Nginx is typically located at /etc/nginx/nginx.conf. Within this file, the http block contains global settings that affect all virtual hosts. Key directives to consider for performance include:

  • worker_processes: This should generally be set to the number of CPU cores available on your server. For optimal performance, it’s often recommended to set this to auto, allowing Nginx to determine the best value.
  • worker_connections: This defines the maximum number of simultaneous connections that each worker process can handle. The system-wide limit is worker_processes * worker_connections. Ensure this value is sufficiently high, but not so high that it exhausts system resources. A common starting point is 1024 or 4096.
  • multi_accept: Setting this to on allows worker processes to accept as many new connections as possible at one time.
  • keepalive_timeout: This directive sets the time a non-active keep-alive connection will remain open. A lower value can free up resources faster, while a higher value can reduce latency for clients making subsequent requests. A value between 65 and 75 seconds is often a good balance.
  • sendfile: Setting this to on allows Nginx to send files directly from the kernel’s page cache, bypassing user space and significantly improving performance for static file delivery.
  • tcp_nopush and tcp_nodelay: These directives can improve the efficiency of data transfer. tcp_nopush on tells Nginx to try and send header and beginning of response in one packet. tcp_nodelay on disables the Nagle algorithm, which can reduce latency by sending small packets immediately.

Here’s an example of an optimized http block:

Example nginx.conf http block

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

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

    # Increase the maximum number of allowed simultaneous connections
    worker_connections 4096;
    multi_accept on;

    # Define the default server and include virtual host configurations
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Optional: Increase client_max_body_size if you expect large file uploads
    # client_max_body_size 100M;

    # Include server block configurations
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Laravel Application Server Block Configuration

For your Laravel application, you’ll typically have a server block (virtual host) configuration. This block defines how Nginx handles requests for your specific domain. Key optimizations include caching static assets, efficient proxying, and handling PHP/Gunicorn requests.

Example Laravel Server Block

server {
    listen 80;
    server_name your_domain.com www.your_domain.com;
    root /var/www/your_laravel_app/public; # Adjust to your Laravel public directory

    index index.php index.html index.htm;

    # Serve static assets directly with aggressive caching
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
        expires 365d;
        add_header Cache-Control "public, immutable";
        access_log off; # Optionally disable access logs for static assets
        try_files $uri $uri/ =404;
    }

    # Handle all other requests by proxying to PHP-FPM or Gunicorn
    location / {
        try_files $uri $uri/ /index.php?$query_string; # For PHP-FPM
        # For Gunicorn/uWSGI:
        # 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;
    }

    # PHP-FPM configuration (if using PHP-FPM)
    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/php8.1-fpm.sock; # Adjust version and path as needed
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to hidden files
    location ~ /\.ht {
        deny all;
    }

    # Error pages
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

Important Notes:

  • Replace your_domain.com and /var/www/your_laravel_app/public with your actual domain and application path.
  • The location ~ \.php$ block is for PHP-FPM. If you are using Gunicorn (for Python/Flask/Django, or even with frameworks like Hyperf/Swoole in PHP), you would use the proxy_pass directives instead.
  • Ensure the fastcgi_pass directive points to your active PHP-FPM socket. Check /etc/php/X.Y/fpm/pool.d/www.conf for the correct socket path.
  • The try_files directive for the root location is crucial for Laravel’s routing. It first checks for the requested URI as a file, then a directory, and finally falls back to index.php.
  • Aggressive caching for static assets using expires 365d and Cache-Control "public, immutable" is vital for performance. immutable tells the browser that the content will never change for this URI, allowing for maximum caching.

Optimizing PHP-FPM for Laravel

When running PHP applications like Laravel, PHP-FPM (FastCGI Process Manager) is the de facto standard. Its performance is heavily influenced by its process management and resource allocation. The primary configuration file is typically /etc/php/X.Y/fpm/php.ini and /etc/php/X.Y/fpm/pool.d/www.conf (where X.Y is your PHP version).

Key PHP-FPM Configuration Directives

In /etc/php/X.Y/fpm/pool.d/www.conf, the pm (Process Manager) settings are paramount:

  • pm: This can be set to static, dynamic, or ondemand.
    • static: A fixed number of child processes are always kept alive. Best for dedicated servers with predictable load.
    • dynamic: Processes are spawned as needed, up to a defined maximum. Good for variable loads.
    • ondemand: Processes are only spawned when a request is received and killed after a period of inactivity. Can save resources but might introduce latency on first request.
  • pm.max_children: The maximum number of child processes that can be spawned (for dynamic and static). This is a critical setting. Too high can exhaust memory; too low can lead to request queuing.
  • pm.start_servers: The number of child processes to start when PHP-FPM starts (for dynamic).
  • pm.min_spare_servers: The minimum number of idle (spare) processes to maintain (for dynamic).
  • pm.max_spare_servers: The maximum number of idle (spare) processes to maintain (for dynamic).
  • pm.process_idle_timeout: The number of seconds after which a child process will be killed if idle (for dynamic and ondemand).
  • request_terminate_timeout: The maximum time in seconds a script is allowed to run before it is terminated. Crucial for preventing runaway scripts.
  • listen.backlog: The number of connections to queue.

A common strategy for dynamic PM is to set pm.max_children based on server memory. A rough guideline is (Total RAM - RAM for OS/Other Services) / Average PHP Process Size. You can monitor average PHP process size with tools like htop or ps aux | grep php-fpm.

Example www.conf for Dynamic PM

; /etc/php/8.1/fpm/pool.d/www.conf

[www]
user = www-data
group = www-data
listen = /var/run/php/php8.1-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
listen.backlog = 511

pm = dynamic
pm.max_children = 100       ; Adjust based on server RAM
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.process_idle_timeout = 10s

; Maximum execution time for scripts
request_terminate_timeout = 60

; Set to 'no' for production to prevent accidental memory leaks
; memory_limit = 256M ; Defined in php.ini, but can be overridden here if needed

; Other settings from php.ini can be tuned here if necessary
; opcache.enable=1
; opcache.memory_consumption=128
; opcache.interned_strings_buffer=16
; opcache.max_accelerated_files=10000
; opcache.revalidate_freq=60
; opcache.validate_timestamps=1
; opcache.enable_cli=1

Tuning php.ini:

Ensure opcache is enabled and configured appropriately in /etc/php/X.Y/fpm/php.ini. This is crucial for Laravel performance as it caches compiled PHP code.

; /etc/php/8.1/fpm/php.ini

; Enable OPcache
opcache.enable=1
opcache.enable_cli=1 ; Enable for CLI scripts too

; Memory allocated to OPcache (adjust based on your needs and server RAM)
opcache.memory_consumption=128

; Number of files to cache. Adjust based on your project size.
opcache.max_accelerated_files=10000

; How often to check for file updates (0 = never, 60 = every 60 seconds)
; For production, setting to 0 and manually clearing cache on deploy is often best.
opcache.revalidate_freq=60
opcache.validate_timestamps=1 ; Set to 0 for production if you manually clear cache

; Other useful settings
opcache.interned_strings_buffer=16
opcache.fast_shutdown=0
opcache.enable_file_override=0

After making changes to PHP-FPM configuration, always restart the service:

sudo systemctl restart php8.1-fpm

Database Tuning: PostgreSQL for Laravel

PostgreSQL is a robust and feature-rich relational database system. Optimizing its performance is critical for any Laravel application that relies heavily on database interactions. The primary configuration file is postgresql.conf, typically found in the data directory (e.g., /etc/postgresql/X.Y/main/postgresql.conf).

Key PostgreSQL Configuration Parameters

Tuning PostgreSQL involves adjusting parameters related to memory, I/O, and query processing. The following parameters are essential:

  • 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 total system RAM, but not exceeding 8GB on older systems or 40% on modern systems with ample RAM.
  • work_mem: The maximum amount of memory to be used for internal sort operations and hash tables before writing to temporary disk files. This affects sorting, hashing, and window functions. Set it too high, and you risk out-of-memory errors if many queries use it simultaneously.
  • maintenance_work_mem: The maximum amount of memory to be used for maintenance operations like VACUUM, CREATE INDEX, and ALTER TABLE ADD FOREIGN KEY. Larger values can significantly speed up these operations.
  • effective_cache_size: This parameter informs the query planner about the total amount of memory available for disk caching by the operating system and PostgreSQL itself. It doesn’t allocate memory but influences query plan choices. A good starting point is 50-75% of total system RAM.
  • checkpoint_completion_target: Controls how aggressively checkpoints are spread out over time. A value of 0.9 spreads them out over 90% of the checkpoint interval, reducing I/O spikes.
  • wal_buffers: Memory for WAL (Write-Ahead Logging) data. A value of -1 means it’s automatically set to 1/3 of shared_buffers, up to 16MB. Often, a value of 16MB is sufficient.
  • max_connections: The maximum number of concurrent connections. Be conservative here, as each connection consumes memory.
  • random_page_cost: This parameter influences the query planner’s cost estimate for fetching a random page from disk. The default is 4.0. If you have fast SSDs, lowering this to 1.1 (or similar) can encourage the planner to use more index scans.

Example postgresql.conf Snippet

# /etc/postgresql/14/main/postgresql.conf (Example for PostgreSQL 14)

# Memory settings
shared_buffers = 2GB             # Adjust based on server RAM (e.g., 25% of 8GB RAM)
work_mem = 32MB                  # Adjust based on typical query complexity and RAM
maintenance_work_mem = 256MB     # For VACUUM, CREATE INDEX, etc.
effective_cache_size = 6GB       # e.g., 75% of 8GB RAM

# Checkpointing
checkpoint_completion_target = 0.9
# wal_buffers = 16MB           # Often auto-tuned well, but 16MB is a common value

# Connections
max_connections = 100            # Adjust based on expected load and server RAM

# Query Planner
random_page_cost = 1.1           # For SSDs

# Logging (adjust as needed for debugging/monitoring)
log_destination = 'stderr'
logging_collector = on
log_directory = 'pg_log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_statement = 'none'           # 'all', 'ddl', 'mod', 'log', 'none'
log_min_duration_statement = 250ms # Log queries longer than 250ms

# Autovacuum settings (crucial for performance and preventing bloat)
autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = 1min
autovacuum_vacuum_threshold = 50
autovacuum_analyze_threshold = 50
autovacuum_vacuum_scale_factor = 0.1  # 10% of table size
autovacuum_analyze_scale_factor = 0.05 # 5% of table size
autovacuum_vacuum_cost_delay = 10ms  # Adjust if autovacuum is too aggressive
autovacuum_vacuum_cost_limit = -1    # Use kernel's default I/O scheduler limits

After modifying postgresql.conf, you must restart the PostgreSQL service:

sudo systemctl restart postgresql

Database Indexing and Query Optimization

Configuration tuning is only part of the story. Effective database performance relies heavily on proper indexing and efficient queries. Laravel’s Eloquent ORM can sometimes generate inefficient queries, especially with eager loading or complex relationships.

Key Strategies:

  • Analyze Queries: Use EXPLAIN ANALYZE in PostgreSQL to understand how your queries are executed. Identify full table scans and missing indexes.
  • Laravel Debugbar: Install the barryvdh/laravel-debugbar package. It provides invaluable insights into database queries executed by your application, including their execution time and number of queries.
  • Eager Loading: Use Eloquent’s eager loading (with(), load()) to fetch related data in a single query rather than N+1 query problems.
  • Selective Columns: Only select the columns you need using select().
  • Indexing: Create appropriate indexes on columns used in WHERE clauses, JOIN conditions, and ORDER BY clauses.

Example: Using EXPLAIN ANALYZE

-- Example query to analyze
SELECT users.name, orders.order_date
FROM users
JOIN orders ON users.id = orders.user_id
WHERE users.is_active = TRUE
ORDER BY orders.order_date DESC;

-- Analyze the query plan
EXPLAIN ANALYZE
SELECT users.name, orders.order_date
FROM users
JOIN orders ON users.id = orders.user_id
WHERE users.is_active = TRUE
ORDER BY orders.order_date DESC;

The output of EXPLAIN ANALYZE will show you the cost and time spent on each step of the query execution. Look for high costs associated with sequential scans on large tables where an index could be beneficial.

Putting It All Together: Deployment and Monitoring

Implementing these tuning strategies requires a systematic approach. After applying changes, it’s crucial to monitor your system’s performance to validate the improvements and identify any new bottlenecks.

Deployment Workflow

  • Version Control: Keep all configuration files (Nginx, PHP-FPM, PostgreSQL) under version control (e.g., Git).
  • Staging Environment: Test all configuration changes on a staging environment that closely mirrors your production setup before deploying to production.
  • Rollback Plan: Have a clear rollback strategy in case a configuration change negatively impacts performance or stability.
  • Automated Deployments: Use tools like Ansible, Chef, or Puppet to automate the deployment of configuration changes.

Monitoring and Alerting

Continuous monitoring is key to maintaining optimal performance and proactively addressing issues. Essential metrics to track include:

  • Server Resources: CPU usage, memory usage, disk I/O, network traffic. Tools: htop, vmstat, Prometheus Node Exporter.
  • Nginx: Active connections, requests per second, error rates (4xx, 5xx). Tools: Nginx status module, Prometheus Nginx Exporter.
  • PHP-FPM: Number of active processes, idle processes, request queue length. Tools: PHP-FPM status page, Prometheus PHP-FPM Exporter.
  • PostgreSQL: Query execution times, cache hit ratios, active connections, replication lag (if applicable), disk usage, bloat. Tools: pg_stat_activity, pg_stat_statements, Prometheus PostgreSQL Exporter.
  • Application Performance Monitoring (APM): Track application-level metrics like response times, error rates, and slow transactions. Tools: New Relic, Datadog, Sentry.

Set up alerts for critical thresholds (e.g., high CPU, low disk space, high error rates) to be notified of potential problems before they impact users.

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

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala