The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on DigitalOcean for Laravel
Nginx Configuration for Laravel Applications
Optimizing Nginx is crucial for serving Laravel applications efficiently. We’ll focus on key directives that impact performance and security, particularly for PHP-FPM or Gunicorn backends.
Static File Serving and Caching
Nginx excels at serving static assets. Proper caching headers reduce load on your application server and improve client-side load times. We’ll leverage expires and add_header directives.
Example Nginx Configuration Snippet
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
log_not_found off;
}
This configuration targets common static file extensions. Setting expires to 30 days and Cache-Control to public, immutable instructs browsers and intermediate caches to aggressively cache these assets. Disabling access logs for static files reduces I/O overhead.
PHP-FPM (or Gunicorn) Proxy Configuration
The core of your application logic is handled by PHP-FPM (for traditional PHP) or Gunicorn (for frameworks like Flask/Django, or even for running Laravel with Octane). Efficient proxying is key.
PHP-FPM Configuration
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust to your PHP-FPM version and socket path
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300; # Increase timeout for long-running scripts
fastcgi_send_timeout 300;
fastcgi_connect_timeout 60;
}
Ensure fastcgi_pass points to your correct PHP-FPM socket. The fastcgi_read_timeout, fastcgi_send_timeout, and fastcgi_connect_timeout directives are critical for preventing timeouts on longer-running requests, common in complex Laravel operations.
Gunicorn Configuration (for Laravel Octane or other Python backends)
location / {
proxy_pass http://127.0.0.1:8000; # Assuming Gunicorn runs on port 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;
proxy_read_timeout 300s;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
}
When using Gunicorn, typically with Laravel Octane, these proxy_set_header directives are essential for passing client information to the backend. Adjust proxy_pass to match your Gunicorn instance’s address and port. Similar to PHP-FPM, tune the timeout values.
Rate Limiting and Security
Protecting your application from abuse is paramount. Nginx’s built-in rate limiting can significantly mitigate brute-force attacks and prevent resource exhaustion.
# In http block
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
# In server or location block
location / {
limit_req zone=mylimit burst=20 nodelay;
# ... other proxy/PHP-FPM directives
}
This example defines a zone named mylimit that stores state for 10MB and allows a maximum of 10 requests per second per IP address. burst=20 allows for a short burst of up to 20 requests, and nodelay ensures that requests exceeding the rate are immediately rejected rather than delayed.
Gunicorn Tuning for Laravel Octane
When running Laravel Octane, Gunicorn acts as the process manager. Tuning Gunicorn’s worker count and settings directly impacts how many concurrent requests your application can handle.
Worker Processes and Threads
The number of worker processes and threads is the most critical tuning parameter. A common starting point is to set the number of workers to (2 * number_of_cpu_cores) + 1. For Octane, which uses asynchronous workers, the concept of threads is less relevant than the number of concurrent workers Gunicorn can manage.
Gunicorn Command Line Arguments
gunicorn --workers 4 \
--worker-class "sync" \
--bind 127.0.0.1:8000 \
--timeout 300 \
--graceful-timeout 300 \
--log-level info \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log \
your_project.wsgi:application # Adjust to your WSGI entry point
For Octane, you’d typically use a custom Gunicorn configuration or a systemd service file. The --workers flag is key. If your application is I/O bound, you might experiment with higher worker counts. If CPU bound, stick closer to the (2 * cores) + 1 rule. --timeout and --graceful-timeout should align with your Nginx proxy timeouts.
Worker Class Considerations
While sync is the default and most common worker class, for I/O-bound applications, gevent or eventlet can offer better concurrency by using green threads. However, for Laravel Octane, which is built on Swoole/OpenSwoole, Gunicorn’s role is primarily process management, and the underlying async capabilities are handled by Swoole itself. Therefore, the sync worker class is often sufficient and simpler to manage.
MySQL Performance Tuning
Database performance is often the bottleneck. Tuning MySQL’s configuration parameters can yield significant improvements.
Key Configuration Parameters
We’ll focus on parameters within my.cnf (or mysqld.cnf) that have the most impact on performance for a typical Laravel workload.
`innodb_buffer_pool_size`
This is arguably the most important parameter for InnoDB. It determines how much memory is allocated for caching data and indexes. A common recommendation is to set it to 70-80% of your available RAM on a dedicated database server.
[mysqld] innodb_buffer_pool_size = 4G # Example for a server with 8GB RAM
`innodb_log_file_size` and `innodb_log_buffer_size`
These parameters affect write performance. Larger log files can improve performance by reducing the frequency of flushing, but also increase recovery time after a crash. The buffer size determines how much data is written to the redo log before being flushed.
[mysqld] innodb_log_file_size = 512M innodb_log_buffer_size = 16M
`query_cache_size` (Deprecated in MySQL 8.0+)
For MySQL versions prior to 8.0, the query cache could sometimes help, but it often became a bottleneck due to invalidation overhead. It’s generally recommended to disable it or set it to a very small size if you are on an older version. For MySQL 8.0+, it’s removed entirely.
[mysqld] query_cache_type = 0 query_cache_size = 0
`max_connections`
This defines the maximum number of simultaneous client connections. Setting this too high can exhaust server memory. Set it based on your application’s needs and server resources. A common starting point is 151, but monitor your actual usage.
[mysqld] max_connections = 200
Slow Query Log Analysis
Identifying slow queries is crucial for optimization. Enable the slow query log and analyze its output regularly.
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 # Log queries taking longer than 2 seconds log_queries_not_using_indexes = 1
Use tools like pt-query-digest from Percona Toolkit to analyze the slow query log and pinpoint problematic SQL statements. Once identified, optimize these queries by adding appropriate indexes or refactoring the application logic.
Monitoring and Iteration
Performance tuning is not a one-time task. Continuous monitoring and iterative adjustments are essential for maintaining optimal performance.
Key Metrics to Monitor
- Nginx: Request rate, error rate (5xx, 4xx), connection count, upstream response times.
- Gunicorn/PHP-FPM: Worker utilization, request queue length, response times, memory usage per worker.
- MySQL: Query throughput, connection count, buffer pool hit rate, slow queries, disk I/O, CPU utilization.
- System: CPU load, memory usage, disk I/O, network traffic.
Tools for Monitoring
Leverage tools like:
- Prometheus & Grafana: For comprehensive time-series monitoring and visualization. Use exporters like
node_exporter,mysqld_exporter, and Nginx/PHP-FPM specific exporters. - Datadog, New Relic, AppDynamics: Commercial APM solutions offering integrated infrastructure and application monitoring.
- Built-in DigitalOcean Metrics: For basic host-level performance insights.
htop,iotop,iftop: For real-time, on-server diagnostics.
Regularly review these metrics. When performance degrades or bottlenecks are identified, revisit the configuration parameters discussed above. Make one change at a time, measure its impact, and iterate. This systematic approach ensures you achieve and maintain a highly performant Laravel application on DigitalOcean.