All cheatsheets

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.

round-robin → app-1
Clientbrowser
nginxTLS terminates
upstream
app-1
app-2
app-3

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 -t

Test the config for syntax errors WITHOUT applying it

nginx -s reload

Apply 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 -T

Dump 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 fallback

How 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