The reverse proxy, conductor of your stack
On most VPS we audit, Nginx is the front door: it terminates TLS, routes to applications, absorbs spikes. A production configuration fits in four blocks — the same ones we recommend through support.
TLS: the non-negotiable base
TLS 1.3, HSTS, and modern parameters. With Let's Encrypt and certbot, renewal is a non-issue:
ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;Keep TLS 1.2 as a fallback while old clients exist; TLS 1.3 saves a full round trip on every new connection.
WebSocket: the two headers everyone forgets
A real-time app that works locally and fails behind the proxy? The upgrade is missing:
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}The proxy_read_timeout keeps Nginx from killing idle connections after 60 seconds.
Rate limiting: your first application firewall
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}Ten requests per second per IP, with a tolerated burst of twenty. Trivial to set up, and it neutralizes most scans and small L7 floods — the rest is absorbed by our upstream anti-DDoS.
Microcaching: one second that changes everything
For near-static pages generated dynamically, a one-second cache divides load by the number of simultaneous visitors:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=micro:10m inactive=60m;
location / {
proxy_cache micro;
proxy_cache_valid 200 1s;
proxy_cache_use_stale updating;
proxy_pass http://127.0.0.1:3000;
}On a CMS-powered blog, that's the difference between 40 req/s and 4,000 req/s on the same VPS.
Production details
proxy_set_header X-Real-IP $remote_addr;so the application sees the real client IPclient_max_body_size 25m;for uploads, without opening the floodgates- Short
proxy_connect_timeout(2 to 5 s): a dead backend must fail fast proxy_buffering on(default): it shields your backends from slow clients
Test every change with nginx -t, reload with systemctl reload nginx — zero downtime, and no excuse for blind production testing.