• 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 MySQL on Linode for Laravel

The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on Linode for Laravel

Nginx as a High-Performance Frontend Proxy

For a Laravel application, Nginx serves as the de facto standard for a high-performance frontend proxy. Its event-driven architecture excels at handling concurrent connections, serving static assets efficiently, and acting as a reverse proxy to your application server (Gunicorn or PHP-FPM). We’ll focus on tuning Nginx for optimal throughput and low latency.

Core Nginx Configuration Tuning

The primary configuration file is typically located at /etc/nginx/nginx.conf. We’ll adjust global settings that impact worker processes and connection handling.

Start by examining the events block. The worker_connections directive is crucial. It defines the maximum number of simultaneous connections that each worker process can handle. A common starting point is to set this to a value that accounts for your expected concurrent users plus some buffer. The theoretical maximum is limited by the system’s file descriptor limit.

Tuning worker_connections and worker_processes

Determine the number of CPU cores available on your Linode instance. Set worker_processes to match this number, or slightly less if you have other resource-intensive services running on the same server. For worker_connections, a good rule of thumb is 1024 per core, but this can be increased significantly if your server has ample RAM and a high file descriptor limit.

To check your system’s file descriptor limit:

ulimit -n

If this limit is too low (e.g., 1024), you’ll need to increase it. Edit /etc/security/limits.conf and add or modify these lines:

* soft nofile 65536
* hard nofile 65536
root soft nofile 65536
root hard nofile 65536

After modifying limits.conf, you’ll need to log out and log back in for the changes to take effect. Then, update your nginx.conf:

# /etc/nginx/nginx.conf

user www-data;
worker_processes auto; # Or set to the number of CPU cores

events {
    worker_connections 16384; # Adjust based on ulimit -n and expected load
    multi_accept on;
}

http {
    # ... other http configurations
}

Optimizing HTTP/2 and Keep-Alive

Enabling HTTP/2 significantly improves performance by allowing multiplexing, header compression, and server push. Ensure your SSL configuration supports it. Also, tune keepalive_timeout and keepalive_requests to reduce the overhead of establishing new TCP connections for subsequent requests from the same client.

# Inside the http block of nginx.conf or a dedicated conf.d file

http {
    # ... other http configurations

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    keepalive_timeout 65;
    keepalive_requests 1000;

    # Enable HTTP/2 (requires SSL)
    # In your server block:
    # listen 443 ssl http2;

    # ... other http configurations
}

Caching Strategies

Nginx excels at caching static assets. Configure appropriate cache headers for your static files (CSS, JS, images) to leverage browser caching and potentially Nginx’s FastCGI cache or proxy cache for dynamic content if applicable.

# Inside your Laravel application's server block

location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp)$ {
    expires 365d;
    add_header Cache-Control "public, immutable";
    access_log off;
    log_not_found off;
}

# For dynamic content caching (use with caution and proper invalidation)
# proxy_cache_path /var/cache/nginx/myapp levels=1:2 keys_zone=myapp_cache:10m max_size=10g inactive=60m use_temp_path=off;
# proxy_cache myapp_cache;
# proxy_cache_valid 200 302 10m;
# proxy_cache_valid 404 1m;
# proxy_cache_key "$scheme$request_method$host$request_uri";
# add_header X-Cache-Status $upstream_cache_status;

Gunicorn/PHP-FPM: The Application Server Tune-Up

The choice between Gunicorn (for Python/WSGI apps, often used with frameworks like Django or Flask, but can proxy PHP via FastCGI) and PHP-FPM (for PHP applications like Laravel) dictates the tuning approach. We’ll cover both.

Gunicorn Tuning for Performance

Gunicorn’s performance is heavily influenced by its worker processes. The most common worker type for I/O-bound applications like web servers is the gevent worker, which uses greenlets for concurrency. For CPU-bound tasks, the sync worker (one process per request) or gthread (multiple threads per process) might be considered, but gevent is generally preferred for typical web workloads.

The number of worker processes should ideally be (2 * number_of_cores) + 1. The --worker-connections (for gevent) or --threads (for gthread) should be set based on your expected concurrency and available RAM. A common starting point for gevent workers is 1000.

# Example Gunicorn startup command or systemd service file

# Assuming 4 CPU cores
# For gevent workers
gunicorn --workers 9 --worker-class gevent --worker-connections 1000 --bind 0.0.0.0:8000 myapp.wsgi:application

# For sync workers (less common for high concurrency)
# gunicorn --workers 4 --worker-class sync --bind 0.0.0.0:8000 myapp.wsgi:application

Ensure your Gunicorn configuration is managed by a process supervisor like systemd for automatic restarts and reliable operation.

PHP-FPM Tuning for Laravel

PHP-FPM (FastCGI Process Manager) is the standard for serving PHP applications. Its configuration is primarily managed in /etc/php/[version]/fpm/pool.d/www.conf (or a custom pool file). The key directives to tune are related to process management and child process lifecycle.

Process Management Modes

PHP-FPM offers three process management modes:

  • static: Pre-forks a fixed number of child processes. Good for predictable loads.
  • dynamic: Starts with a few processes and spawns more up to a `pm.max_children` limit as needed.
  • ondemand: Starts no children initially and spawns them only when requests arrive. Lowest memory footprint but highest latency for the first request.

For most Laravel applications on Linode, dynamic or static are the preferred choices. dynamic offers a good balance between resource utilization and responsiveness.

Key PHP-FPM Directives

pm.max_children: The maximum number of child processes that will be spawned. This is the most critical setting. It should be calculated based on available RAM and the memory footprint of your Laravel application per process. A common formula is (Total RAM - RAM for OS/Nginx) / Average RAM per PHP process.

pm.start_servers: The number of child processes to start when PHP-FPM starts. For dynamic, this is the initial number.

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.max_requests: The number of requests each child process should execute before respawning. This helps mitigate memory leaks in PHP extensions or the application itself. A value between 500 and 1000 is typical.

; /etc/php/[version]/fpm/pool.d/www.conf

[www]
user = www-data
group = www-data
listen = /run/php/php[version]-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 50       ; Adjust based on RAM and app footprint
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 10
pm.max_requests = 500

; For static mode:
; pm = static
; pm.max_children = 20     ; Adjust based on RAM and app footprint

; For ondemand mode (use with caution for high-traffic sites):
; pm = ondemand
; pm.max_children = 50
; pm.process_idle_timeout = 10s
; pm.max_requests = 500

After modifying PHP-FPM configuration, restart the service:

sudo systemctl restart php[version]-fpm

MySQL/MariaDB Performance Tuning

Database performance is often the bottleneck. Tuning MySQL/MariaDB involves adjusting key configuration parameters in /etc/mysql/my.cnf or /etc/mysql/mariadb.conf.d/50-server.cnf.

Key MySQL/MariaDB Variables

innodb_buffer_pool_size: This is arguably the most important setting for InnoDB. It’s the memory area where InnoDB caches table data and indexes. Set this to 70-80% of your available RAM if MySQL is the primary service on the server. If you have other services (like Nginx, PHP-FPM), allocate less.

innodb_log_file_size: Controls the size of the redo log files. Larger log files can improve write performance by reducing the frequency of flushing dirty pages, but increase recovery time after a crash. A common starting point is 256M or 512M. Changing this requires a clean shutdown and restart of MySQL, and potentially manual removal of old log files.

innodb_flush_log_at_trx_commit: Controls how often the log buffer is flushed to the log file.

  • 1 (default): Flush on every commit. Safest for ACID compliance, but slowest.
  • 0: Flush every second. Faster, but you might lose up to 1 second of transactions in a crash.
  • 2: Flush on every commit, but let the OS write it to disk. Faster than 1, safer than 0.
For most web applications, setting this to 2 offers a good balance between performance and durability. If absolute data integrity is paramount, stick with 1.

max_connections: The maximum number of simultaneous client connections. Set this based on your application’s needs and server resources. Too high can lead to resource exhaustion.

query_cache_size and query_cache_type: The query cache is deprecated in MySQL 5.7 and removed in MySQL 8.0. If you are on an older version, it *might* offer benefits for read-heavy workloads with identical queries, but it can also be a source of contention. For modern Laravel applications, it’s generally recommended to disable it and rely on application-level caching (e.g., Redis, Memcached) or Nginx caching.

# /etc/mysql/my.cnf or /etc/mysql/mariadb.conf.d/50-server.cnf

[mysqld]
# General Settings
max_connections = 200
# ... other general settings

# InnoDB Settings
innodb_buffer_pool_size = 4G       ; Adjust based on available RAM (e.g., 70-80% of RAM if dedicated)
innodb_log_file_size = 512M        ; Requires MySQL restart and potentially log file cleanup
innodb_flush_log_at_trx_commit = 2 ; Good balance for web apps
innodb_file_per_table = 1          ; Recommended for easier management and fragmentation control
innodb_io_capacity = 200           ; Adjust based on disk I/O capabilities (e.g., 200 for SSDs)
innodb_io_capacity_max = 400       ; Adjust based on disk I/O capabilities

# Query Cache (Deprecated/Removed in newer MySQL versions)
# query_cache_size = 0
# query_cache_type = 0

# Other potentially useful settings
tmp_table_size = 64M
max_heap_table_size = 64M
sort_buffer_size = 4M
join_buffer_size = 4M
read_rnd_buffer_size = 4M

After modifying MySQL configuration, restart the service:

sudo systemctl restart mysql

Monitoring and Iteration

Performance tuning is an iterative process. Use monitoring tools like htop, iotop, mysqltuner.pl, and application performance monitoring (APM) solutions to identify bottlenecks. Regularly review Nginx access logs, PHP-FPM logs, and MySQL slow query logs. 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

  • 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