The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Elasticsearch on OVH for Laravel
Nginx as a High-Performance Frontend for Laravel
When deploying Laravel applications, Nginx serves as an excellent choice for a web server and reverse proxy. Its event-driven, asynchronous architecture makes it highly efficient for handling concurrent connections. For optimal performance, especially with PHP-FPM or Gunicorn, careful configuration is paramount. We’ll focus on tuning Nginx for static file serving, SSL termination, and efficient proxying to your application server.
Core Nginx Configuration for Laravel
The primary configuration file for Nginx is typically located at /etc/nginx/nginx.conf. Within this, we define worker processes and global settings. For a typical OVH VPS, setting worker_processes to the number of CPU cores is a good starting point. worker_connections dictates the maximum number of simultaneous connections a worker can handle; a value of 4096 is common and generally sufficient.
user www-data;
worker_processes auto; # Or set to the number of CPU cores
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Important for security
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip compression
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Include virtual host configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Laravel Site-Specific Configuration
For each Laravel application, a dedicated server block (virtual host) is recommended. This file is typically placed in /etc/nginx/sites-available/your-app.conf and then symlinked to /etc/nginx/sites-enabled/. Key directives include caching for static assets, proper proxying, and handling of PHP requests.
Static File Caching and Compression
Efficiently serving static assets (CSS, JS, images) is crucial. We’ll leverage browser caching and Nginx’s built-in compression.
server {
listen 80;
server_name your-domain.com www.your-domain.com;
root /var/www/your-app/public; # Adjust to your Laravel project's public directory
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Caching for static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 365d;
add_header Cache-Control "public, no-transform";
access_log off; # Optional: reduce log noise for static files
}
# PHP-FPM configuration (if using PHP-FPM)
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Adjust to your PHP-FPM socket or IP:port
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
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;
}
}
Tuning Gunicorn for Laravel (with Swoole/Octane) or PHP-FPM
The application server configuration depends on your Laravel deployment strategy. For traditional PHP-FPM, tuning is done within PHP-FPM’s pool configuration. If you’re using Laravel Octane with Swoole or RoadRunner, Gunicorn (or a similar WSGI server) might be involved, or Swoole/RoadRunner directly.
PHP-FPM Tuning
PHP-FPM configuration is typically found in /etc/php/8.1/fpm/pool.d/www.conf (adjust version and pool name as needed). The pm (process manager) setting is critical. dynamic is often a good balance, but ondemand can save resources if traffic is sporadic. For high-traffic sites, static might offer the best raw performance but requires careful memory management.
; /etc/php/8.1/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock ; Or a TCP/IP socket like 127.0.0.1:9000 listen.owner = www-data listen.group = www-data listen.mode = 0660 ; Process Manager settings pm = dynamic pm.max_children = 50 ; Max number of children serving requests pm.start_servers = 5 ; Number of children created at startup pm.min_spare_servers = 5 ; Min number of idle respawners pm.max_spare_servers = 10 ; Max number of idle respawners pm.process_idle_timeout = 10s ; Timeout for idle processes to be killed (ondemand only) pm.max_requests = 500 ; Max requests per child before respawning ; Other important settings request_terminate_timeout = 60s ; Timeout for script execution ; memory_limit = 256M ; Adjust based on your application's needs ; post_max_size = 64M ; upload_max_filesize = 64M
Tuning Strategy: Start with dynamic. Monitor server load (CPU, RAM) and PHP-FPM status (using pm.status_path in Nginx). If you see processes constantly being created and destroyed, increase min_spare_servers and max_spare_servers. If memory is an issue, reduce max_children and potentially switch to ondemand. For predictable high load, static with carefully calculated pm.max_children based on available RAM can yield lower latency.
Gunicorn Tuning (for Python-based apps or as a proxy)
If you’re using Gunicorn as a WSGI server for a Python framework (less common for direct Laravel, but possible with tools like python-laravel or for proxying other services), tuning involves worker count and type. For a PHP application served via a WSGI interface (e.g., RoadRunner), Gunicorn might act as an intermediary.
# Example Gunicorn command gunicorn --workers 4 --worker-class gevent --bind 127.0.0.1:8000 your_app.wsgi:application
Worker Count: A common recommendation is (2 * number_of_cores) + 1. However, this is highly dependent on whether your workers are CPU-bound or I/O-bound. For I/O-bound tasks (like waiting for database or external API calls), asynchronous workers (like gevent or eventlet) are more efficient. For CPU-bound tasks, synchronous workers (like sync or gthread) might be better, but require more processes.
Elasticsearch Performance Tuning on OVH
Elasticsearch is a powerful search engine, but it can be resource-intensive. Proper tuning is essential to avoid performance bottlenecks, especially on shared or VPS environments like OVH.
JVM Heap Size Configuration
The most critical tuning parameter for Elasticsearch is the JVM heap size. It’s configured in /etc/elasticsearch/jvm.options. The rule of thumb is to set the initial (-Xms) and maximum (-Xmx) heap size to 50% of the available system RAM, but **never exceeding 30-31 GB**. Exceeding this threshold can lead to issues with compressed ordinary object pointers (compressed oops).
# /etc/elasticsearch/jvm.options # Example for a server with 16GB RAM -Xms8g -Xmx8g # Example for a server with 64GB RAM (DO NOT exceed ~31GB for heap) # -Xms16g # -Xmx16g
Important: After changing jvm.options, you must restart the Elasticsearch service: sudo systemctl restart elasticsearch.
Filesystem Cache and Swapping
Elasticsearch relies heavily on the operating system’s filesystem cache. Ensure that Elasticsearch is not being swapped out. Add the following to /etc/elasticsearch/elasticsearch.yml to disable swapping for the Elasticsearch process:
# /etc/elasticsearch/elasticsearch.yml bootstrap.memory_lock: true
You also need to configure the system to allow memory locking. Edit /etc/security/limits.conf:
# /etc/security/limits.conf elasticsearch soft memlock unlimited elasticsearch hard memlock unlimited
And ensure the Elasticsearch user is allowed to lock memory. Edit /etc/elasticsearch/jvm.options and uncomment or add:
# /etc/elasticsearch/jvm.options -XX:-UseCompressedOops
Note: -XX:-UseCompressedOops is generally not recommended unless you are hitting the 32GB limit. If you are not hitting that limit, keep it enabled. The primary goal is to prevent the OS from swapping Elasticsearch’s memory. After these changes, restart Elasticsearch.
Index and Shard Optimization
The number of shards and replicas significantly impacts performance and resource usage. Avoid over-sharding. A common recommendation is to keep shard sizes between 10GB and 50GB. Monitor shard sizes and adjust your indexing strategy accordingly. For Laravel applications, consider using the Elasticsearch Index Lifecycle Management (ILM) feature to automate index management (e.g., rolling over indices based on size or age).
// Example ILM policy (via Elasticsearch API)
PUT _ilm/policy/my_laravel_logs_policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_age": "7d",
"max_primary_shard_size": "50gb"
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
Apply this policy to your index templates. Also, limit the number of replicas to 1 or 0 on development/testing environments to save resources.
Monitoring and Diagnostics
Continuous monitoring is key to identifying and resolving performance issues. Utilize tools like:
- Nginx:
stub_statusmodule for connection counts,access.loganderror.loganalysis (e.g., usinggoaccessor ELK stack). - PHP-FPM:
pm.status_pathin Nginx to view active processes, requests, etc. - Elasticsearch: Elasticsearch’s own monitoring APIs (
_catAPIs,_nodes/stats,_cluster/stats), and tools like Kibana’s Stack Monitoring. - System Resources:
htop,iotop,vmstat,sarfor CPU, memory, I/O, and network usage.
For Laravel-specific issues, enable detailed logging and use tools like Laravel Telescope or the built-in logging to pinpoint slow database queries, external API calls, or inefficient code execution.