Cheatsheets
Nginx
Location matching, reverse proxy, TLS, proxy headers, caching, and rate limiting.
Visualize
nginx — reverse proxy & load balancing
nginx terminates TLS and forwards each request to an upstream server, round-robin.
client side · HTTPS (encrypted) upstream side · HTTP
Press play — the client opens an HTTPS connection to nginx.
48 entries
Config structure & lifecycle9
# /etc/nginx/nginx.conf — the four nesting levels
user www-data;
worker_processes auto; # main context
events { worker_connections 1024; } # connection-handling
http { # http context: all web/proxy config
include mime.types;
default_type application/octet-stream;
server { # one virtual host
listen 80;
server_name example.com;
location / { # one URL match within the host
root /var/www/html;
}
}
}The directive hierarchy: main → events → http → server → location
include mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;Pull other files into the current context (supports globs)
nginx -tTest the config for syntax errors WITHOUT applying it
nginx -s reloadApply config changes with zero downtime (graceful reload)
nginx -s quit # graceful stop
nginx -s stop # fast shutdown
nginx -s reopen # reopen log files (after logrotate)Control signals sent to the master process
nginx -TDump the FULL effective config (all includes resolved)
http {
gzip on; # set once at http level …
server {
# … inherited here, unless overridden:
location /api/ { gzip off; } # override for this location only
}
}Directive inheritance: outer contexts cascade inward
# Custom log format with timing + upstream info
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent rt=$request_time '
'uct=$upstream_connect_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;Logging: access_log, error_log, and a timing-aware log_format
worker_processes auto; # one worker per CPU core
worker_rlimit_nofile 65535; # raise the per-process FD limit
events {
worker_connections 4096; # max simultaneous conns per worker
multi_accept on;
}Worker tuning: processes, connections, and the file-descriptor ceiling
Listening & host matching8
listen 80; # IPv4 :80
listen [::]:80; # IPv6 :80
listen 443 ssl; # HTTPS
listen 443 ssl default_server; # catch-all for unmatched Host
http2 on; # HTTP/2 (nginx ≥ 1.25.1)Which address/port a server block binds to
server_name example.com www.example.com; # exact
server_name *.example.com; # leading wildcard
server_name ~^api\d+\.example\.com$; # regex (note the ~)
server_name _; # catch-all (matches nothing real)Match the request's Host header to pick a server block
# location matching PRECEDENCE — nginx picks ONE, in this order:
location = /exact { } # 1. exact match wins immediately
location ^~ /assets/ { } # 2. prefix that STOPS regex search if longest
location ~ \.php$ { } # 3. case-sensitive regex (first match in file order)
location ~* \.(jpg|png)$ { } # 3. case-insensitive regex
location / { } # 4. plain prefix — longest one is remembered as fallbackHow nginx chooses a location (a top interview/debug topic)
server {
root /var/www/site; # filesystem base for this server
location /img/ {
# root APPENDS the full URI: /img/cat.png → /var/www/site/img/cat.png
root /var/www/site;
}
location /static/ {
# alias REPLACES the matched prefix: /static/app.js → /opt/assets/app.js
alias /opt/assets/;
}
}root vs alias — the single most common nginx file-path bug
# SPA: serve the file, then the dir, then fall back to index.html
location / {
try_files $uri $uri/ /index.html;
}try_files: serve the first path that exists, else fall back
index index.html index.htm;Which file to serve when the URI is a directory
# Permanent redirect with a captured path segment
location /old/ {
rewrite ^/old/(.*)$ /new/$1 permanent; # 301 /old/x → /new/x
}
# Internal rewrite (no redirect sent to the browser)
rewrite ^/v1/(.*)$ /api/$1 last;rewrite vs return: regex URL rewriting
location /downloads/ {
autoindex on; # generate an HTML directory listing
autoindex_exact_size off;
autoindex_localtime on;
}autoindex: auto-generate a directory listing
Reverse proxy7
# WITH trailing slash → nginx replaces the matched location prefix
location /api/ {
proxy_pass http://127.0.0.1:3000/; # /api/users → http://127.0.0.1:3000/users
}
# WITHOUT trailing slash → the full original URI is forwarded
location /api/ {
proxy_pass http://127.0.0.1:3000; # /api/users → http://127.0.0.1:3000/api/users
}proxy_pass: the trailing slash changes the upstream path (classic trap)
upstream backend {
least_conn; # balancing algorithm (default: round-robin)
server 10.0.0.1:3000 weight=3; # gets 3x the traffic
server 10.0.0.2:3000;
server 10.0.0.3:3000 backup; # only used when others are down
keepalive 32; # pooled idle conns to upstreams
}
server {
location / { proxy_pass http://backend; }
}upstream {}: a named pool of backends with load balancing
location / {
proxy_pass http://backend;
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;
proxy_set_header X-Forwarded-Host $host;
}The standard proxy header block — pass client info to the backend
# Define ONCE at http level:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1; # required for upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s; # don't kill idle sockets
}
}WebSocket proxying — the map + Upgrade/Connection headers
location / {
proxy_pass http://backend;
proxy_connect_timeout 5s; # time to establish the TCP conn to upstream
proxy_send_timeout 60s; # gap allowed while sending to upstream
proxy_read_timeout 60s; # gap allowed while reading the response
}Proxy timeouts: connect vs send vs read
location / {
proxy_pass http://backend;
proxy_buffering on; # buffer the upstream response (default)
proxy_buffers 8 16k;
proxy_buffer_size 16k;
}
# Turn OFF for streaming / Server-Sent Events:
location /stream { proxy_pass http://backend; proxy_buffering off; }Proxy buffering: read the backend fast, drip to slow clients
upstream backend {
server a:3000 max_fails=3 fail_timeout=30s;
server b:3000 max_fails=3 fail_timeout=30s;
}
location / {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
}Passive health checks & failover (max_fails, proxy_next_upstream)
TLS / HTTPS6
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}ssl_certificate + ssl_certificate_key: the cert and its private key
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # let TLS 1.3 clients choose
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;Protocols & ciphers: a modern, safe baseline
# 301 every http request to the same URL on https
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}HTTP→HTTPS redirect: the canonical one-liner
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;HSTS: force browsers to use HTTPS for this domain
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;OCSP stapling: prove the cert isn't revoked, fast
# /.well-known/acme-challenge must reach the webroot, NOT the https redirect
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / { return 301 https://$host$request_uri; }
}Let's Encrypt http-01: exempt the ACME challenge from the redirect
Performance & caching7
gzip on;
gzip_comp_level 5; # 1 (fast) … 9 (smallest); 4–6 is the sweet spot
gzip_min_length 256; # don't bother compressing tiny responses
gzip_vary on; # emit Vary: Accept-Encoding for caches
gzip_types text/plain text/css application/json application/javascript
application/xml image/svg+xml;gzip: compress text responses on the fly
location ~* \.(?:css|js|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}Long-lived caching for fingerprinted static assets
# 1) Declare the cache zone at http level:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m
max_size=1g inactive=60m use_temp_path=off;
server {
location / {
proxy_pass http://backend;
proxy_cache app_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status; # HIT / MISS / EXPIRED
}
}proxy_cache: cache upstream responses on disk
sendfile on;
tcp_nopush on;
tcp_nodelay on;Kernel-level send optimisations
upstream backend {
server 10.0.0.1:3000;
keepalive 32; # idle conns to KEEP open to upstream
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # required for upstream keepalive
proxy_set_header Connection ""; # clear the close-by-default header
}
}Keepalive to the upstream — reuse backend connections
# Pre-compressed assets: serve app.js.gz / app.js.br if present
gzip_static on;
location ~* \.(?:js|css)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}gzip_static / brotli_static: serve files compressed at build time
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_errors on;Cache file descriptors & metadata for static serving
Security & rate limiting7
# 1) Declare a shared zone at http level (10MB ≈ 160k IPs):
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
server {
location /login {
limit_req zone=login burst=10 nodelay;
proxy_pass http://backend;
}
}limit_req: token-bucket rate limiting per client
limit_conn_zone $binary_remote_addr zone=perip:10m;
server {
location /download/ {
limit_conn perip 3; # max 3 concurrent connections per client IP
}
}limit_conn: cap concurrent connections per key
client_max_body_size 50m;Max allowed request body — the silent 413 on uploads
location /admin/ {
allow 10.0.0.0/8; # office / VPN range
allow 203.0.113.4; # a specific bastion
deny all; # everyone else → 403
proxy_pass http://backend;
}allow / deny: IP-based access control
server_tokens off;Hide the nginx version from responses & error pages
location /private/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
}HTTP Basic auth: a quick username/password gate
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 Content-Security-Policy "default-src 'self'" always;Common security response headers
Real-world server blocks4
# Reverse proxy a Node/Express app on :3000 with TLS + WS
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri; # force HTTPS
}
server {
listen 443 ssl;
http2 on;
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;
client_max_body_size 25m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}Complete HTTPS reverse proxy to a Node app (with WebSocket support)
# SPA static files + /api proxied to a backend
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/spa/dist;
index index.html;
# Long-cache the fingerprinted bundle
location ~* \.(?:css|js|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API goes to the backend (prefix stripped by the trailing slash)
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Everything else falls back to the SPA shell
location / {
try_files $uri $uri/ /index.html;
}
}Single-page app: serve static build, proxy /api, fall back to index.html
# Canonicalise www → apex (and keep HTTPS)
server {
listen 443 ssl;
http2 on;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri; # drop the www
}Redirect www → apex (one canonical hostname)
# Maintenance mode: 503 everyone except your IP
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
set $maintenance on;
if ($remote_addr = 203.0.113.7) { set $maintenance off; } # your IP bypasses
location / {
if ($maintenance = on) { return 503; }
proxy_pass http://127.0.0.1:3000;
}
error_page 503 /maintenance.html;
location = /maintenance.html { root /var/www/errors; internal; }
}Maintenance / kill-switch page that lets your own IP through