Nginx Reverse Proxy Configuration That Holds Under Load
Nginx sits between the internet and your app servers, terminating TLS, serving static assets, and load balancing upstream PHP-FPM or Node processes. Misconfigured buffers and missing security headers cause more outages than application bugs.
Production Server Block
upstream laravel_backend {
server 127.0.0.1:9000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
root /var/www/app/public;
index index.php;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass laravel_backend;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 60s;
}
location ~* \.(js|css|png|jpg|svg|woff2)$ {
expires 30d;
access_log off;
}
}
Tuning and Gotchas
- Set
client_max_body_sizefor file uploads—default 1MB breaks invoices - Enable gzip for JSON and SVG, not already-compressed images
- Use
proxy_set_header X-Forwarded-Proto $schemebehind ALB - Rate limit login routes with
limit_req_zone
Reload with nginx -t && systemctl reload nginx—never restart blindly in production without syntax check.
SSL and HTTP/2 Tuning
Use Mozilla SSL Configuration Generator profiles—Intermediate for most sites, Modern if you drop old Android clients. Enable OCSP stapling to shave TLS handshake latency. Renew Let's Encrypt certs via certbot timer; monitor expiry 30 days ahead.
Separate access and error logs per vhost for easier debugging. Log format including request_time and upstream_response_time identifies whether slowness is nginx, PHP-FPM, or database.
For WebSocket upgrades behind nginx, proxy_http_version 1.1, Upgrade and Connection headers are mandatory. Missing config manifests as connects that work locally but fail in staging behind proxy.
Enable brotli_types including application/json for API responses over 1KB—mobile clients on slow networks benefit measurably. Monitor 499 client closed connection rates; spikes often indicate upstream slowness causing users to abandon before response completes.
High Availability Patterns
Run two nginx instances behind keepalived or cloud load balancer for HA—single nginx VM is SPOF entire site depends on. Health check nginx itself with external monitor hitting static health.html bypassing upstream.
Separate configs per site in sites-enabled with include snippets for SSL params—DRY ssl_protocols definition prevents updating five vhosts individually when TLS guidance changes next year.
Rate-limit login endpoints even on non-WordPress stacks—bots scan universally. A modest limit_req zone cuts brute-force noise and log spam without affecting legitimate users who mistype passwords occasionally.
Logging and Privacy
Mask or omit sensitive query parameters from access logs—tokens in URLs end up in log aggregators and SIEM forever. Use custom log_format including request_id forwarded from application for end-to-end tracing across nginx and PHP-FPM logs during incident response.
Test config with nginx -t in CI before deploy—syntax errors during reload take down entire site if init script does not validate first. Keep previous config file timestamped backup on server for one-command rollback when deploy goes wrong at peak traffic hour.
Map custom error pages for 502/503—default nginx error HTML screams misconfiguration to users; branded maintenance page preserves trust during brief upstream outages while team fixes PHP-FPM pool exhaustion.
Document who approves TLS cipher changes and where configs live in git. Compliance scans flag legacy protocols before enterprise audits; having an owner and runbook saves frantic searches the week before assessors arrive.
Keep a staging vhost mirroring production TLS settings—test cert renewals there before certbot touches live traffic and accidentally breaks chain completeness.
Always run nginx -t before reload.
WebSocket Proxying
For Laravel Echo or Socket.io, add upgrade headers on the proxy location. Log rotation matters—unbounded access logs filled a client's disk in eleven days during a traffic spike.
Map custom 502/503 error pages to branded maintenance screens—default nginx error HTML erodes user trust during brief upstream outages.
Frequently Asked Questions
Nginx vs Apache for Laravel?
Nginx + PHP-FPM handles concurrent connections more efficiently. Apache mod_php is simpler but heavier per connection.
How do I proxy to Node on port 3000?
Use proxy_pass http://127.0.0.1:3000 with WebSocket upgrade headers for Socket.io.
Should I terminate SSL at nginx or ALB?
ALB termination simplifies cert management on AWS. Nginx termination gives more control on VPS setups.