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.
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.