The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and DynamoDB 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 a Laravel application, we’ll focus on optimizing Nginx for static file serving, SSL termination, and efficient proxying to our PHP process manager.
Nginx Configuration for Laravel
A robust Nginx configuration for a Laravel application typically involves several key directives. We’ll assume a standard OVH setup with PHP-FPM and a single-page application (SPA) routing pattern, common for modern Laravel apps.
Core Server Block
This block defines the primary listening ports, server name, and SSL configuration. Ensure your SSL certificates are correctly placed and referenced.
server {
listen 80;
listen [::]:80;
server_name your_domain.com www.your_domain.com;
return 301 https://$host$request_uri; # Redirect HTTP to HTTPS
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your_domain.com www.your_domain.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s; # Google DNS, adjust as needed
resolver_timeout 5s;
# Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; # Adjust as needed
# Gzip Compression
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 image/svg+xml;
# Root and Index
root /var/www/your_laravel_app/public;
index index.php index.html index.htm;
# Static File Caching
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;
}
# PHP-FPM Configuration
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and socket path
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300; # Increase timeout for long-running PHP scripts
}
# Deny access to .env and other sensitive files
location ~ /\.env { deny all; }
location ~ /\.git { deny all; }
location ~ /\.env\.example { deny all; }
location ~ /\.htaccess { deny all; }
location ~ /\.composer { deny all; }
location ~ /\.config { deny all; }
# Laravel SPA Routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Error Pages
error_page 404 /index.php; # Laravel handles 404s
error_page 500 502 503 504 /index.php; # Laravel handles these too
}
Key Optimizations Explained:
- HTTP/2: Enabled for faster multiplexing and header compression.
- SSL Configuration: Strong cipher suites and TLS versions are crucial. OCSP stapling improves handshake performance.
- Security Headers: Essential for protecting against common web vulnerabilities.
- Gzip Compression: Reduces bandwidth usage and speeds up asset delivery.
- Static File Caching: Long expiry times for static assets with `immutable` cache control.
- PHP-FPM Proxy: Directs PHP requests to the FPM worker pool.
fastcgi_read_timeoutis increased to prevent timeouts on lengthy operations. - SPA Routing:
try_filesdirective ensures that all non-file requests are passed toindex.php, allowing Laravel’s router to handle them. - Sensitive File Protection: Prevents direct access to configuration and source files.
Gunicorn/PHP-FPM Tuning for Laravel
For PHP applications like Laravel, PHP-FPM (FastCGI Process Manager) is the standard. Tuning its configuration is critical for handling load effectively. We’ll focus on the php-fpm.conf and pool configuration files.
PHP-FPM Pool Configuration
The pool configuration (e.g., /etc/php/8.1/fpm/pool.d/www.conf) dictates how PHP processes are managed. The choice between static, dynamic, and ondemand process management depends on your application’s traffic patterns and server resources.
; /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 ; Process Manager Settings ; pm = dynamic ; Default pm = ondemand ; Recommended for variable traffic, conserves resources when idle ; pm = static ; Best for constant high traffic, but uses more memory ; Dynamic Process Management ; pm.max_children = 50 ; pm.start_servers = 5 ; pm.min_spare_servers = 2 ; pm.max_spare_servers = 10 ; pm.process_idle_timeout = 10s ; OnDemand Process Management pm.max_children = 100 ; Maximum number of children that can be started. pm.start_time = '200s' ; The time after which a child process may be killed if inactive. pm.max_requests = 500 ; Maximum number of requests each child process should execute. ; Other important settings request_terminate_timeout = 300 ; Corresponds to fastcgi_read_timeout in Nginx ; rlimit_files = 4096 ; rlimit_nofile = 65536 ; pm.max_children = 100 ; Adjust based on available RAM. Rule of thumb: (Total RAM - OS/Nginx RAM) / Average PHP Process RAM ; pm.max_requests = 1000 ; Prevents memory leaks from accumulating over time.
Tuning Parameters:
pm:dynamic: PHP-FPM spawns processes as needed, up topm.max_children. Good balance.ondemand: Processes are only created when a request comes in and are killed after a timeout (pm.process_idle_timeoutorpm.start_time). Excellent for reducing memory usage during low traffic.static: A fixed number of processes are always kept alive. Best for predictable, high-traffic scenarios.pm.max_children: This is the most critical setting. It directly impacts memory usage. Calculate this based on your server’s RAM. A common approach:(Total RAM - (OS RAM + Nginx RAM + MySQL RAM)) / Average PHP Process RAM. Monitor memory usage withhtopor similar tools.pm.max_requests: Setting this to a reasonable value (e.g., 500-1000) helps mitigate memory leaks in long-running PHP processes.request_terminate_timeout: Should generally match or be slightly higher than Nginx’sfastcgi_read_timeoutto avoid premature termination of long-running scripts.
DynamoDB Performance Tuning for Laravel
When using DynamoDB with Laravel, especially for caching or session storage, performance hinges on proper table design, provisioned throughput, and efficient querying. OVH does not directly offer DynamoDB, so this section assumes you are using AWS DynamoDB or a compatible managed service.
Table Design and Indexing
A well-designed DynamoDB table is fundamental. Choose your Partition Key (PK) and Sort Key (SK) wisely to distribute data evenly and support your query patterns.
- Partition Key (PK): Aim for high cardinality. Avoid hot partitions where a single PK receives a disproportionate amount of traffic.
- Sort Key (SK): Enables efficient range queries and sorting within a partition.
- Global Secondary Indexes (GSIs): Use GSIs to support query patterns that don’t align with the base table’s keys. Be mindful that GSIs consume their own provisioned throughput.
- Local Secondary Indexes (LSIs): Useful for supporting different sort orders within the same partition key. LSIs share the PK with the base table and have a different SK. They must be created at table creation time.
Provisioned Throughput Management
DynamoDB operates on a provisioned throughput model (Read Capacity Units – RCUs, Write Capacity Units – WCUs). Over-provisioning is expensive, while under-provisioning leads to throttling.
- Monitor Usage: Regularly check CloudWatch metrics for
ConsumedReadCapacityUnitsandConsumedWriteCapacityUnits. - Identify Throttling: Look for
ReadThrottleEventsandWriteThrottleEvents. - Auto Scaling: Configure DynamoDB Auto Scaling to automatically adjust provisioned throughput based on actual usage. This is crucial for cost optimization and performance stability.
{
"TableName": "YourLaravelCacheTable",
"KeySchema": [
{ "AttributeName": "pk", "KeyType": "HASH" },
{ "AttributeName": "sk", "KeyType": "RANGE" }
],
"AttributeDefinitions": [
{ "AttributeName": "pk", "AttributeType": "S" },
{ "AttributeName": "sk", "AttributeType": "S" }
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 10,
"WriteCapacityUnits": 10
},
"GlobalSecondaryIndexes": [
{
"IndexName": "GSI1",
"KeySchema": [
{ "AttributeName": "gsi1pk", "KeyType": "HASH" },
{ "AttributeName": "gsi1sk", "KeyType": "RANGE" }
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
}
]
}
Auto Scaling Configuration Example (AWS CLI):
# Enable Auto Scaling for the main table
aws application-autoscaling register-scalable-target \
--service-namespace dynamodb \
--resource-id table/YourLaravelCacheTable \
--scalable-dimension dynamodb:table:WriteCapacityUnits \
--min-capacity 5 \
--max-capacity 50
aws application-autoscaling register-scalable-target \
--service-namespace dynamodb \
--resource-id table/YourLaravelCacheTable \
--scalable-dimension dynamodb:table:ReadCapacityUnits \
--min-capacity 5 \
--max-capacity 50
# Define scaling policies
aws application-autoscaling put-scaling-policy \
--service-namespace dynamodb \
--resource-id table/YourLaravelCacheTable \
--scalable-dimension dynamodb:table:WriteCapacityUnits \
--policy-name WriteCapacityScalingPolicy \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "DynamoDBWriteCapacityUtilization"
},
"ScaleInCooldown": 60,
"ScaleOutCooldown": 60
}'
aws application-autoscaling put-scaling-policy \
--service-namespace dynamodb \
--resource-id table/YourLaravelCacheTable \
--scalable-dimension dynamodb:table:ReadCapacityUnits \
--policy-name ReadCapacityScalingPolicy \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "DynamoDBReadCapacityUtilization"
},
"ScaleInCooldown": 60,
"ScaleOutCooldown": 60
}'
The TargetValue of 70% is a common starting point, allowing headroom for spikes while scaling down aggressively when idle. Adjust min-capacity and max-capacity based on your expected traffic and budget.
Efficient Querying in Laravel
When interacting with DynamoDB from Laravel, use the AWS SDK for PHP. Optimize your queries to minimize consumed capacity.
use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\Marshaler;
$marshaler = new Marshaler();
$client = new DynamoDbClient([
'region' => 'us-east-1', // Your AWS region
'version' => 'latest',
// Add credentials or IAM role configuration here
]);
// Example: Storing session data
$sessionId = session()->getId();
$sessionData = session()->all(); // Be mindful of session data size
$params = [
'TableName' => 'YourLaravelSessionTable',
'Item' => $marshaler->marshalItem([
'pk' => 'SESSION#' . $sessionId,
'sk' => 'METADATA',
'data' => serialize($sessionData), // Serialize for storage
'expires_at' => time() + config('session.lifetime') * 60, // TTL in seconds
]),
];
try {
$client->putItem($params);
} catch (AwsException $e) {
// Handle error
error_log("DynamoDB Session Save Error: " . $e->getMessage());
}
// Example: Retrieving session data
$sessionId = session()->getId();
$params = [
'TableName' => 'YourLaravelSessionTable',
'Key' => $marshaler->marshalItem([
'pk' => 'SESSION#' . $sessionId,
'sk' => 'METADATA',
]),
];
try {
$result = $client->getItem($params);
if (isset($result['Item'])) {
$item = $marshaler->unmarshalItem($result['Item']);
// Unserialize and load session data
session()->replace(unserialize($item['data']));
}
} catch (AwsException $e) {
// Handle error
error_log("DynamoDB Session Load Error: " . $e->getMessage());
}
// Example: Using a GSI for querying
// Assume GSI1 is configured with gsi1pk and gsi1sk
$params = [
'TableName' => 'YourLaravelDataTable',
'IndexName' => 'GSI1',
'KeyConditionExpression' => 'gsi1pk = :pk_val AND gsi1sk BETWEEN :sk_start AND :sk_end',
'ExpressionAttributeValues' => $marshaler->marshalItem([
':pk_val' => 'USER#123',
':sk_start' => 'ORDER#2023-01-01',
':sk_end' => 'ORDER#2023-12-31',
]),
];
try {
$result = $client->query($params);
// Process $result['Items']
} catch (AwsException $e) {
// Handle error
error_log("DynamoDB Query Error: " . $e->getMessage());
}
Key Considerations:
- Serialization: For session data or complex objects, serialization (like PHP’s
serialize()or JSON encoding) is often necessary. Be mindful of the size of serialized data, as it impacts storage costs and read/write times. - TTL (Time To Live): DynamoDB’s TTL feature is excellent for automatically expiring session data or cache entries, reducing manual cleanup and storage costs.
- Query vs. Scan: Always prefer
Queryoperations overScan. Scans read the entire table and are very inefficient and costly. Design your keys and indexes to support efficient queries. - Batch Operations: Use
BatchGetItemandBatchWriteItemfor multiple read/write operations to reduce network latency and improve efficiency, but be aware of their limitations (e.g., 25 items per batch).