The Nginx Config Directives I Actually Reach For: A Practical Nginx Cheat Sheet
A working Nginx cheat sheet covering server and location blocks, reverse proxy, redirects, gzip, TLS, static files, plus how to test and reload config safely.
The Nginx Config Directives I Actually Reach For: A Practical Nginx Cheat Sheet
Most of an Nginx config is forgettable. You write it once, it works, and you never look at it again until something breaks at a bad hour. The trouble is that the handful of directives you do touch repeatedly — server, location, proxy_pass, return 301, gzip, ssl_certificate — each carry a small trap that bites you exactly when you have no patience for it. This post is the short list of Nginx config I keep within reach, with the gotchas spelled out, so you can paste a working block instead of stitching one together from five tabs.
Server and location blocks: the skeleton
Every Nginx site is a server block, and routing inside it happens through location blocks. A minimal static site looks like this:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/site;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
The part that surprises people is location matching priority. Nginx does not read top to bottom and stop at the first match. The order is: = exact match wins first, then ^~ preferential prefix, then regex (~ case-sensitive, ~* case-insensitive) in the order written, and finally the longest plain prefix. So location = /favicon.ico always beats location ~ \.ico$, no matter where you put it. When a request goes somewhere you did not expect, this priority list is almost always the reason.
One more skeleton trap: root versus alias. With root /var/www/site; inside location /static/, a request for /static/app.js maps to /var/www/site/static/app.js. With alias /var/www/site/; it maps to /var/www/site/app.js — the matched prefix is dropped. Mixing them up is how a perfectly valid config serves 404s for files that clearly exist on disk.
Reverse proxy: the four headers and one slash
This is the directive set I touch most. Proxying to an upstream app is three lines plus the headers that let the app see who actually made the request:
location / {
proxy_pass http://localhost:3000;
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;
}
Without those headers your app logs every request as coming from 127.0.0.1, rate limiting by IP becomes useless, and any "redirect to HTTPS" logic in the app misfires because it thinks the connection is plain HTTP.
The famous trailing-slash gotcha lives here. proxy_pass http://backend/ (with a slash) strips the matched location prefix — a request to /api/users arrives at the backend as /users. proxy_pass http://backend (no slash) keeps it — the backend still sees /api/users. Pick the wrong one and you get 404s that look like the backend is broken when the path is simply mangled in transit. When in doubt, curl the backend directly and watch its access log instead of guessing.
WebSockets need two extra lines or the handshake hangs at "Pending" forever:
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
Redirects, gzip, and static caching
For redirects, reach for return before rewrite. A clean HTTP-to-HTTPS jump is one line in a dedicated block:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
return 301 is permanent, return 302 is temporary, and return 410 tells crawlers a page is gone for good. Reserve rewrite for when you genuinely need to transform the path, and skip if inside a location — it has scope rules surprising enough that the Nginx wiki literally titles a page "If Is Evil."
Compression and caching are cheap wins. Enable gzip with explicit types (Nginx only gzips text/html by default):
gzip on;
gzip_types text/plain text/css application/json application/javascript image/svg+xml;
gzip_min_length 1024;
For hashed static assets, tell the browser to cache hard:
location ~* \.(?:js|css|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
That immutable flag stops repeat visitors from even sending a revalidation request, which trims real latency off every page after the first.
TLS: fullchain or nothing
The single most common TLS mistake is pointing ssl_certificate at the leaf certificate alone. That works in your browser and fails on Android and older clients, which is the worst kind of bug because it passes your own smoke test.
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
}
Always use fullchain.pem, not cert.pem. If you run certbot --nginx, Certbot wires this up correctly for you and even adds the renewal cron — let it. Hold off on HSTS until HTTPS has been stable for a week, and start with a short max-age before going anywhere near preload, because HSTS mistakes are painful to undo on mobile.
Test and reload without taking the site down
This is the muscle memory worth building. Never reload a config you have not validated. The two-command ritual is:
nginx -t && nginx -s reload
nginx -t parses the full config and reports the exact file and line of any syntax error, and the && guarantees the reload only happens if the test passes. A reload is graceful — Nginx spins up new worker processes with the new config and lets old workers finish their in-flight requests, so live traffic never sees a blip. Compare that with nginx -s stop (immediate, cuts connections) versus nginx -s quit (graceful shutdown). For day-to-day changes you want reload, every time.
When something is still wrong after a clean reload, nginx -T dumps the entire merged config with every include resolved, which is how you find the stray server block in conf.d/ that is shadowing the one you just edited.
A worked scenario: Node app behind Nginx
Here is the flow I run on a fresh VPS. The Node app already listens on 127.0.0.1:3000. I drop one server block into /etc/nginx/sites-available/app.conf:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://localhost:3000;
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;
}
}
Then symlink it into sites-enabled, validate, and reload:
ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
nginx -t && nginx -s reload
If I get a 502 Bad Gateway, I do not touch timeouts — a 502 means Nginx reached the upstream and got nothing usable, so the backend is the suspect. I curl -v http://127.0.0.1:3000 to confirm the app is even alive, then tail -f /var/log/nginx/error.log to read the real reason: "connect() failed (111: Connection refused)" means the app is not listening, "upstream prematurely closed connection" means it crashed mid-request. Bumping proxy_read_timeout fixes 504s, never 502s — keeping those two straight has saved me more 2am minutes than any other single fact in this list. Once HTTP works, certbot --nginx -d app.example.com adds TLS and the HTTP-to-HTTPS redirect in one step.
Quick reference
- Validate then reload:
nginx -t && nginx -s reload - Dump merged config:
nginx -T - Graceful stop:
nginx -s quit(notstop) - Reverse proxy:
proxy_pass+ the fourX-Forwarded/Host/X-Real-IPheaders - Trailing slash on
proxy_passstrips the location prefix; no slash keeps it - Redirect:
return 301 https://$host$request_uri; - SPA fallback:
try_files $uri $uri/ /index.html; - TLS: always
fullchain.pem, never the leaf cert - 502 = backend problem; 504 = timeout; do not confuse the fixes
I keep all of this — 80+ directives, the pitfalls, and six paste-ready full templates — searchable in the Nginx Cheatsheet, so when an alert fires I type "502" or "proxy_pass" and the exact entry comes up with the fix attached. If your stack runs in containers, the Docker Cheatsheet pairs naturally with it for the layer underneath Nginx.
Made by Toolora · Updated 2026-06-13