DS DevShelfHub Projects · AI tools
Cheatsheets / Nginx
Cheatsheet · Dev tooling

Nginx: proxy_pass, TLS, Upstreams and Rate Limiting Reference Guide

By DevShelfHub

Virtual hosts, proxy_pass, SSL/TLS with Let's Encrypt, rate limiting, gzip, caching, load balancing, and security headers — nginx reference for reverse proxies, API gateways, and static file servers.

101 items 8 min server location upstream

Start hereQuick start · 6 you’ll reach for daily

Test confignginx -t
Reloadsystemctl reload nginx
Server blockserver { … }
Routelocation / { … }
Proxyproxy_pass http://…
Tailtail -F …/error.log

Target versions · paceVersions

Targets: nginx ≥ 1.24 (stable) HTTP/2 stable, HTTP/3 with quic module OpenSSL 3.x

Snippets target nginx 1.24+ on a Debian-style layout (/etc/nginx/sites-enabled). HTTP/2 is one keyword (http2); HTTP/3 needs nginx built with the QUIC module (1.25+) and uses listen 443 quic. Legacy listen 443 ssl spdy; syntax is gone.

install · control · logsSetup

bash
# Install
apt install nginx                      # Debian / Ubuntu
brew install nginx                     # macOS dev
docker run -d -p 80:80 nginx:stable    # one-shot

# Config layout (Debian)
/etc/nginx/nginx.conf                  # main; includes sites-enabled/*
/etc/nginx/sites-available/            # write configs here
/etc/nginx/sites-enabled/              # symlinks to sites-available
/etc/nginx/conf.d/                     # alternative drop-in dir
/var/log/nginx/{access,error}.log      # default logs

# Daily-loop
nginx -t                               # validate config; ALWAYS run before reload
systemctl reload nginx                 # apply config without dropping connections
systemctl restart nginx                # full restart (drops in-flight)
nginx -s reload / -s stop              # if not using systemd
nginx -V                               # version + compile flags + modules

# Tail logs
tail -F /var/log/nginx/{access,error}.log

how files fit togetherConfig structure

main contextTop-level: user, worker_processes, events, http.
events { worker_connections 1024; }Per-worker connection cap.
http { … }Where all HTTP servers live.
server { … }A virtual host. Multiple per http.
location PATTERN { … }URL-prefix routing inside a server.
upstream NAME { … }Pool of backend servers.
map / geo / split_clientsLookup-table directives, top-level in http.
include path/*.confGlob-load other files.
stream { … }TCP/UDP proxying (separate from HTTP).

virtual hostsServer blocks

listen 80 / listen 443 ssl http2Port + protocol. http2 implied SSL prior; now explicit.
listen [::]:80IPv6. Pair with v4 listen.
listen 80 default_serverCatch-all when host doesn’t match.
server_name api.example.com www.example.comMultiple names; wildcards (*.example.com) allowed.
server_name ~^api\\.(?<tenant>\\w+)\\.…$Regex names with named captures.
root /var/www/siteFilesystem base for static files.
index index.htmlDefault file for directory requests.
access_log path / offPer-server access log routing.
error_log path leveldebug / info / notice / warn / error.
return 301 https://$host$request_uriClassic HTTP → HTTPS redirect.
bash
# /etc/nginx/sites-available/app.conf
server {
    listen      80 default_server;
    listen      [::]:80 default_server;
    server_name api.example.com www.example.com;

    # Redirect HTTP -> HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen              443 ssl http2;
    listen              [::]:443 ssl http2;
    server_name         api.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;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options    "nosniff"  always;
    add_header X-Frame-Options           "DENY"     always;

    root /var/www/api;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /healthz { return 200 "ok\n"; access_log off; }
}

URL routingLocation

location /path { … }Prefix match.
location = /exact { … }Exact match. Highest priority.
location ^~ /static { … }Prefix match, win over regex.
location ~ \\.php$ { … }Case-sensitive regex.
location ~* \\.(jpg|png)$ { … }Case-insensitive regex.
try_files $uri $uri/ /index.htmlSPA fallback. Try files, then directories, then index.
alias /srv/files/Map URL path to filesystem path (trailing slash matters).
rewrite ^/old/(.*)$ /new/$1 permanent301 rewrite.
internalMarks a location for internal redirects only.
return 410 / 444410 gone; 444 close the connection (custom).

reverse proxyingproxy_pass

proxy_pass http://backendForward to an upstream.
proxy_pass http://10.0.0.10:8000/Trailing slash strips the location prefix.
proxy_http_version 1.1Required for keepalive + WebSocket upgrades.
proxy_set_header Host $hostForward original Host. Default forwards $proxy_host.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_forAppend client IP to the chain.
proxy_set_header X-Forwarded-Proto $schemeTell the backend it was HTTPS.
proxy_connect_timeout / proxy_send_timeout / proxy_read_timeoutThree separate timeouts.
proxy_buffering on/offOff for streaming / SSE.
proxy_next_upstream error timeout http_502Retry conditions to the next upstream.
proxy_intercept_errors onLet nginx’s error_page handle backend errors.
proxy_request_buffering offStream the request body. Big uploads / SSE.
bash
upstream app_backend {
    least_conn;                         # alt: ip_hash, hash $remote_addr consistent
    keepalive 32;
    server 10.0.0.10:8000 max_fails=3 fail_timeout=10s;
    server 10.0.0.11:8000 max_fails=3 fail_timeout=10s;
    server 10.0.0.12:8000 backup;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # ... TLS config ...

    location /api/ {
        proxy_pass            http://app_backend;
        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      Connection        "";   # required for keepalive

        proxy_connect_timeout 5s;
        proxy_send_timeout    30s;
        proxy_read_timeout    30s;

        proxy_buffering       on;
        client_max_body_size  8m;
    }

    # WebSocket upgrade
    location /ws/ {
        proxy_pass         http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade    $http_upgrade;
        proxy_set_header   Connection "upgrade";
        proxy_read_timeout 1h;
    }
}

HTTPS, HSTS, OCSPSSL / TLS

ssl_certificate / ssl_certificate_keyFullchain + private key paths.
ssl_protocols TLSv1.2 TLSv1.3Drop everything older. Especially SSLv3 / TLSv1.0.
ssl_ciphersUse Mozilla’s intermediate / modern profiles.
ssl_prefer_server_ciphers off (TLS 1.3)Modern advice. Order doesn’t matter under TLS 1.3.
ssl_session_cache shared:SSL:10mSpeed up resumed sessions.
ssl_stapling on; ssl_stapling_verify on;OCSP stapling. Faster cert validation client-side.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" alwaysHSTS — once over HTTPS, always over HTTPS.
certbot --nginx -d example.comLet’s Encrypt cert issuance + auto-renewal.
openssl s_client -connect host:443Debug handshakes.

load balancingUpstreams

upstream name { server …; }Group of backends.
server host:port weight=NWeighted round-robin (default algorithm).
least_connSend to the connection-lightest backend.
ip_hashSticky by client IP. Useful for session pinning.
hash $request_uri consistentCustom hash key + consistent hashing.
server … max_fails=3 fail_timeout=10sMark unhealthy temporarily.
server … backupOnly used when primaries are down.
server … downManual maintenance flag.
keepalive 32Idle backend connections to keep open per worker.
resolver 1.1.1.1 valid=30sDNS resolver for runtime hostname lookups.

leaky-bucket controlsRate limiting

limit_req_zone $binary_remote_addr zone=NAME:10m rate=20r/sDefine a shared-memory zone keyed by IP.
limit_req zone=NAME burst=40 nodelayApply per-location. nodelay serves bursts immediately.
limit_req_status 429Send 429 Too Many Requests instead of default 503.
limit_conn_zone / limit_connConcurrent-connection limits (vs request rate).
geo $whitelisted { default 0; 10.0.0.0/8 1; }Per-CIDR booleans for skipping limits.
map $whitelisted $limit_key { 1 ""; 0 $binary_remote_addr; }Skip limits for whitelisted ranges.

proxy cache + staticCaching

proxy_cache_path /var/cache/nginx keys_zone=app:50mDefine a cache. Top-level in http.
proxy_cache appUse it inside a location.
proxy_cache_valid 200 1hTTL by upstream status.
proxy_cache_use_stale error timeout updatingServe stale on backend pain. Often the right call.
proxy_cache_bypass $http_pragmaConditions that bypass the cache.
expires 30d / 1y immutablePer-location browser cache headers.
add_header Cache-Control "public, max-age=…"Explicit. Use over implicit expires when ambiguous.
etag on / offEnable ETags on static responses.

compressiongzip & brotli

gzip onEnable.
gzip_types text/plain text/css application/json application/javascriptWhat to compress. HTML is implicit.
gzip_min_length 1024Don’t bother with tiny responses.
gzip_comp_level 51–9. 5 is a strong default; 9 burns CPU for little gain.
gzip_proxied anyCompress proxied responses too.
gzip_vary onAdd Vary: Accept-Encoding.
brotli on (with ngx_brotli)Better compression ratios. Module not in stock nginx.
Static-precompressed: gzip_static onServe pre-built file.gz when present.

URLsRewrites

return 301 https://…Preferred over rewrite for whole-URL redirects.
rewrite ^/old/(.*)$ /new/$1 permanent301.
rewrite ^/old/(.*)$ /new/$1 redirect302.
rewrite ^/old/(.*)$ /new/$1 lastInternal rewrite; re-evaluate location.
rewrite ^/old/(.*)$ /new/$1 breakInternal rewrite; continue in this location.
if ($host = www.example.com)Use if only inside server for redirects. Avoid elsewhere.
$args / $arg_nameQuery string and individual query params.
Avoid if in location blocks. It interacts badly with other directives and the official guidance is “if is evil”. Use map or split into multiple servers/locations.

observabilityLogs

log_format json '{"t":"$time_iso8601","ip":"$remote_addr",…}'JSON access logs — trivial to ship to ELK / Loki.
access_log /var/log/nginx/access.log json buffer=32k flush=5sBuffered async write.
access_log offPer-location silence (e.g. /healthz).
error_log path warnSeverity threshold. Drop to info when debugging.
$upstream_response_time / $request_timeBackend vs total time. Diff = nginx-side overhead.
$upstream_addr / $upstream_statusWhich backend handled what.
tail -F …/error.logAlways have this open during config edits.

tls · rate limit · proxy · gzipEnd-to-end · Prod API host

HTTP redirect, TLS, gzip, rate limit, WebSocket-friendly proxy to an upstream with health-checking. One file, paste-ready.

bash
# /etc/nginx/conf.d/api.conf — full prod config in one file.
# Rate-limit, gzip, security headers, TLS, proxy with health-checked upstream.

limit_req_zone $binary_remote_addr zone=api_rl:10m rate=20r/s;

upstream api {
    keepalive 32;
    server 10.0.0.10:8000 max_fails=3 fail_timeout=10s;
    server 10.0.0.11:8000 max_fails=3 fail_timeout=10s;
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen      80;
    server_name api.example.com;
    return 301  https://$host$request_uri;
}

server {
    listen              443 ssl http2;
    server_name         api.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    gzip            on;
    gzip_types      text/plain text/css application/json application/javascript;
    gzip_min_length 1024;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location /healthz { return 200 "ok\n"; access_log off; }

    location / {
        limit_req zone=api_rl burst=40 nodelay;

        proxy_pass            http://api;
        proxy_http_version    1.1;
        proxy_set_header      Host              $host;
        proxy_set_header      X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header      X-Forwarded-Proto $scheme;
        proxy_set_header      Connection        $connection_upgrade;
        proxy_set_header      Upgrade           $http_upgrade;
        proxy_read_timeout    30s;
        client_max_body_size  8m;
    }
}

Best practiceGood to know

Always nginx -t before reload. A typo in production drops zero connections if you catch it pre-reload — or every connection if you don’t. Wire it into your deploy script.
reload, not restart. Reload swaps config without dropping in-flight requests. Restart kills connections. Only restart when changing core worker settings or the binary.
Prefer map over if. map is evaluated at config time, side-effect-free, and composes. if inside location creates a hidden nested location and breaks several other directives.

Common trapsWatch out for

Trailing slash in proxy_pass changes semantics. proxy_pass http://api; forwards the full URI. proxy_pass http://api/; strips the location prefix. Get this wrong and routes 404 silently.
if inside location — just don’t. Some directives behave differently or are ignored inside if. The pinned “If is Evil” page documents which.
Default listen 443 can’t be reused on a different IP family without [::]:443. A common “why is IPv6 not working” mystery. Always pair listen 443 with listen [::]:443.

Go deeperSee also

Nginx FAQ

What is Nginx used for?

Nginx is a high-performance web server, reverse proxy, and load balancer. It serves static files directly, proxies requests to upstream application servers (Node.js, Django, Rails), handles TLS termination, rate limiting, caching, and HTTP/2 and HTTP/3 connections.

What is the difference between nginx server blocks and Apache virtual hosts?

Both map domain names to document roots. Nginx server blocks use the server { } directive and are event-driven with a single master and worker-process model, making it more efficient under high concurrency. Apache uses per-request threads or processes and supports .htaccess per-directory overrides.

How does proxy_pass work in nginx?

proxy_pass forwards an incoming request to an upstream server and returns the response to the client. Set proxy_set_header Host $host and proxy_set_header X-Real-IP $remote_addr to pass original request metadata. Use an upstream block to define a pool of backend servers for load balancing.

How do I configure TLS in nginx?

In the server block, add listen 443 ssl http2, ssl_certificate /path/to/cert.pem, and ssl_certificate_key /path/to/key.pem. Use Let's Encrypt with Certbot to automate certificate issuance and renewal. Always redirect port 80 to 443 with a return 301 directive.

How does nginx rate limiting work?

Define a shared memory zone with limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s in http { }. Apply it with limit_req zone=api burst=20 nodelay in a location block. The burst value allows a short spike above the rate; nodelay returns 503 immediately instead of queuing excess requests.

How do I set up HTTPS with Let's Encrypt and nginx?

Install Certbot (apt install certbot python3-certbot-nginx), then run certbot --nginx -d yourdomain.com. Certbot edits your nginx config to add ssl_certificate, ssl_certificate_key, and a 301 redirect from HTTP to HTTPS. Set up auto-renewal with a cron job or systemd timer: certbot renew --quiet runs every 60 days and reloads nginx automatically.