Skip to main content

Nginx Cheatsheet — 80+ Configs and Commands with Real Examples and Gotchas

Nginx cheat sheet — common configs, location/server blocks, SSL, reverse proxy, gzip, real examples & gotchas.

  • Runs locally
  • Category Developer & DevOps
  • Best for Formatting, validating, shrinking, or inspecting code-adjacent text.
157 items
CLI commands (17)
nginx -t

Test the current configuration files for syntax errors without reloading nginx.

Common pitfall: Run this BEFORE every reload. A typo plus a blind reload can take production down in two seconds.

Examples
nginx -t
sudo nginx -t
nginx -t -c /etc/nginx/nginx.conf
nginx -s reload

Reload the configuration without dropping in-flight connections (graceful, zero downtime).

Common pitfall: Reload silently keeps the old config if the new one fails to parse. Always pair with `nginx -t` first.

Examples
sudo nginx -s reload
sudo systemctl reload nginx
nginx -t && nginx -s reload
nginx -s stop

Fast shutdown — close listening sockets and kill workers immediately.

Common pitfall: `stop` cuts in-flight requests. For graceful shutdown use `nginx -s quit` instead.

Examples
sudo nginx -s stop
sudo systemctl stop nginx
nginx -s quit

Graceful shutdown — workers finish in-flight requests then exit.

Examples
sudo nginx -s quit
nginx -s reopen

Reopen log files. Use this AFTER logrotate moves access.log / error.log, so nginx writes to the new files.

Common pitfall: Without `reopen`, nginx keeps writing to the rotated (now-deleted-by-name) file descriptor and disk fills up silently.

Examples
sudo nginx -s reopen
postrotate
    /usr/sbin/nginx -s reopen
endscript
nginx -V

Print nginx version, build options, and every module compiled in. Capital V — small v is just the version.

Common pitfall: Use `-V` to confirm an obscure module (`--with-http_v2_module`, `--with-http_realip_module`) is actually built in before you spend an hour debugging "directive unknown".

Examples
nginx -V
nginx -V 2>&1 | tr -- - "\n" | grep _module
nginx -T

Test the config AND dump the fully-resolved, all-includes-merged config to stdout. Best way to see what nginx actually sees.

Examples
sudo nginx -T
sudo nginx -T | grep server_name
sudo nginx -T > /tmp/nginx-full.conf
nginx -c <file>

Start nginx with an alternative config file (handy when testing a candidate config out-of-tree).

Examples
nginx -c /tmp/nginx-test.conf
nginx -t -c /tmp/nginx-test.conf
nginx -p <prefix>

Override the install prefix at runtime — useful for running nginx as a non-root user from a checkout.

Examples
nginx -p $(pwd) -c $(pwd)/nginx.conf
systemctl status nginx

On systemd hosts, check the running state, recent logs, and last exit reason of the nginx service.

Examples
sudo systemctl status nginx
sudo systemctl restart nginx
sudo journalctl -u nginx -n 100 --no-pager
nginx -g <directives>

Inject global config directives from the command line, before the config file is read. Handy for one-off overrides.

Common pitfall: Each directive must end with a semicolon and the whole thing is one quoted argument: `nginx -g "daemon off;"`. Used a lot to run nginx in the foreground inside Docker.

Examples
nginx -g "daemon off;"
nginx -g "worker_processes 2;"
nginx -g "daemon off; master_process off;"
nginx -s reload (HUP signal)

The reload signal is plain SIGHUP. `nginx -s reload` and `kill -HUP <master_pid>` do exactly the same thing.

Examples
kill -HUP $(cat /run/nginx.pid)
nginx -s reload
kill -HUP `cat /run/nginx.pid`
kill -USR1 (reopen logs)

SIGUSR1 tells the master to reopen log files. Identical to `nginx -s reopen`; logrotate scripts often send it directly.

Examples
kill -USR1 $(cat /run/nginx.pid)
kill -USR1 `pidof -s nginx`
kill -USR2 (upgrade binary)

SIGUSR2 starts a new master from a new nginx binary without dropping connections. The classic zero-downtime binary upgrade.

Common pitfall: After USR2 you have two masters. Send WINCH to the old master to drain its workers, verify, then QUIT the old master. Roll back by HUP-ing the old one if the new binary misbehaves.

Examples
kill -USR2 $(cat /run/nginx.pid)
# old pid file becomes nginx.pid.oldbin
kill -WINCH $(cat /run/nginx.pid.oldbin)
kill -QUIT $(cat /run/nginx.pid.oldbin)
nginx -v

Print just the nginx version string. Lowercase v — capital V also dumps build options and modules.

Examples
nginx -v
nginx -v 2>&1
nginx -q

Suppress non-error messages during configuration testing. Pair with -t for quiet CI checks.

Examples
nginx -t -q
nginx -tq && nginx -s reload
nginx -e <file>

Override the error-log path at startup (nginx ≥ 1.19.5). Useful when the default log path is not yet writable.

Examples
nginx -e /dev/stderr -g "daemon off;"
nginx -t -e stderr
Core directives (21)
worker_processes auto;

Number of worker processes. `auto` matches the CPU core count and is the right answer almost always.

Common pitfall: Setting it higher than the core count adds context-switch overhead and gives ZERO extra throughput.

Examples
worker_processes auto;
worker_processes 4;
worker_connections 1024;

Max concurrent connections per worker. Total cap ≈ worker_processes × worker_connections.

Common pitfall: If you also raise this, raise the OS file-descriptor limit (`worker_rlimit_nofile` and systemd `LimitNOFILE`) or nginx logs "too many open files".

Examples
events {
    worker_connections 1024;
}
events {
    worker_connections 10240;
    multi_accept on;
    use epoll;
}
events { ... }

Block for connection-handling settings: worker_connections, multi_accept, use (epoll/kqueue).

Examples
events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}
http { ... }

Top-level block for HTTP-server settings, MIME types, gzip, upstream pools, server blocks.

Examples
http {
    include /etc/nginx/mime.types;
    sendfile on;
    keepalive_timeout 65;
    include /etc/nginx/conf.d/*.conf;
}
include <path>;

Include another config file (glob patterns supported). Standard way to split vhosts.

Common pitfall: Include order matters when directives override each other. `include conf.d/*.conf` loads alphabetically, so `00-default.conf` runs first.

Examples
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
user www-data;

OS user that worker processes run as. Master runs as root, workers drop to this user.

Common pitfall: If nginx workers cannot read your static files you usually see "Permission denied" in error.log. Check ownership against this user.

Examples
user www-data;
user nginx;
user nobody nogroup;
pid /run/nginx.pid;

Path where nginx writes the master PID — used by `nginx -s` signals.

Examples
pid /run/nginx.pid;
pid /var/run/nginx.pid;
sendfile on;

Use the kernel sendfile() syscall — zero-copy from disk to socket. Big win for static files.

Common pitfall: Disable sendfile in VirtualBox shared folders or some old NFS mounts — files come up corrupted otherwise.

Examples
sendfile on;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;

How long an idle keep-alive connection stays open (seconds). 65 covers the default 60s browser timer plus jitter.

Examples
keepalive_timeout 65;
keepalive_timeout 75s;
client_max_body_size 10m;

Max request body size (uploads). Default 1m. Set on http, server, or location.

Common pitfall: Hit a 413 Request Entity Too Large on upload? It is this. Set high enough or upgrade to streaming uploads.

Examples
client_max_body_size 10m;
client_max_body_size 100m;
client_max_body_size 0;  # unlimited
worker_rlimit_nofile 65535;

Raise the per-worker file-descriptor limit. Required when you bump worker_connections.

Examples
worker_rlimit_nofile 65535;
multi_accept on;

Let each worker accept all new connections at once instead of one per event-loop pass. Off by default.

Common pitfall: It helps under bursty connection storms but can mildly hurt latency under steady load. Measure before turning it on in production.

Examples
events {
    multi_accept on;
    worker_connections 4096;
}
use epoll;

Pick the connection-processing method. `epoll` on Linux, `kqueue` on BSD/macOS. nginx auto-detects, so set it only to be explicit.

Examples
events { use epoll; }
events { use kqueue; }  # BSD / macOS
tcp_nopush on;

Send response headers and the start of a file in one packet (Linux TCP_CORK). Only effective when sendfile is on.

Examples
sendfile on;
tcp_nopush on;
tcp_nodelay on;

Disable Nagle’s algorithm on keep-alive connections so small responses go out immediately. On by default.

Examples
tcp_nodelay on;
server_tokens off;

Hide the nginx version number from error pages and the Server response header. A cheap, standard hardening step.

Common pitfall: This hides the version but does not remove the `Server: nginx` header itself. To drop the header entirely you need the headers-more module or a build patch.

Examples
http {
    server_tokens off;
}
types_hash_max_size 2048;

Sizing for the MIME-type hash table. nginx itself tells you to raise it in a startup warning when you add many custom types.

Examples
types_hash_max_size 2048;
types_hash_bucket_size 128;
default_type application/octet-stream;

Fallback Content-Type when nginx cannot guess it from the file extension. octet-stream means "download me".

Examples
include /etc/nginx/mime.types;
default_type application/octet-stream;
client_body_timeout 12s;

Max idle time between two reads of the request body. A slow-upload client that stalls longer gets a 408.

Examples
client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 10s;
large_client_header_buffers 4 8k;

Number and size of buffers for large request headers. Raise it when long cookies or URLs trigger 400 / 414.

Common pitfall: A request line longer than the buffer size gives 414 Request-URI Too Large; oversized headers give 400. Both point here, not at client_max_body_size.

Examples
large_client_header_buffers 4 16k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
resolver 1.1.1.1 valid=300s;

DNS resolver nginx uses to resolve names at runtime (upstreams given as variables, OCSP, auth_request). Cache each answer for `valid`.

Common pitfall: Without a `resolver`, `proxy_pass https://$backend` with a variable host fails with "no resolver defined". A literal hostname is resolved once at startup and does not need it.

Examples
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
resolver_timeout 5s;
Server block (12)
server { ... }

Defines one virtual host. Multiple server blocks let one nginx serve many domains on the same port.

Examples
server {
    listen 80;
    server_name example.com;
    root /var/www/example;
}
listen 80;

Port (and optionally address + flags) this server block accepts on. Add `default_server` to handle requests for unknown hosts.

Common pitfall: Exactly ONE server per port can be `default_server`. Two defaults = nginx -t fails. Forgetting it = first server alphabetically wins.

Examples
listen 80;
listen 443 ssl http2;
listen 80 default_server;
listen [::]:80;  # IPv6
server_name example.com www.example.com;

Which Host header(s) this server block answers. Supports wildcards (*.example.com) and regex (~^api\..+$).

Common pitfall: Wildcard server_names cannot start AND end with `*` (e.g. `*.example.*` is invalid). Regex names must start with `~`.

Examples
server_name example.com;
server_name example.com www.example.com;
server_name *.example.com;
server_name ~^(?<sub>.+)\.example\.com$;
root /var/www/html;

Document root for this server / location. Set once at server level, override in specific locations if needed.

Examples
root /var/www/html;
root /home/user/site/public;
index index.html index.htm;

List of default files to serve when the URI maps to a directory. First one that exists wins.

Examples
index index.html;
index index.html index.htm index.php;
listen 443 ssl http2;

Listen on 443 with TLS and HTTP/2. The single line that turns on both for a server block.

Common pitfall: On nginx ≥ 1.25 prefer the newer form: `listen 443 ssl;` plus `http2 on;` directive — the inline `http2` is deprecated in newer builds.

Examples
listen 443 ssl http2;
listen 443 ssl;
http2 on;
listen [::]:443 ssl http2;
return 444;

Drop the connection without sending a response. Useful for dropping requests with the wrong Host header.

Examples
# Catch-all server that drops unknown hosts
server {
    listen 80 default_server;
    server_name _;
    return 444;
}
listen 80 reuseport;

Enable SO_REUSEPORT so the kernel load-balances new connections across workers. Reduces lock contention at high connection rates.

Common pitfall: Set `reuseport` on only ONE listen directive per address:port. Repeating it on a second server block for the same socket fails to start.

Examples
listen 80 reuseport;
listen 443 ssl reuseport;
listen unix:/run/nginx.sock;

Listen on a Unix domain socket instead of a TCP port. Common for an internal nginx fronted by another nginx or HAProxy on the same host.

Examples
listen unix:/run/nginx-internal.sock;
server_name _;

The "invalid" catch-all name. It matches no real Host header, so combined with default_server it handles everything that no named server claimed.

Common pitfall: `_` is not a wildcard — it only works as a catch-all because it can never match. The actual catch-all behaviour comes from `default_server` on the listen line.

Examples
server {
    listen 80 default_server;
    server_name _;
    return 444;
}
absolute_redirect off;

Make nginx emit relative Location headers on auto-redirects (e.g. adding a trailing slash) instead of absolute URLs with the wrong host/port.

Common pitfall: Behind a proxy that terminates TLS, nginx’s auto-redirect can send `http://internal:8080/path/`. Either set this off, or set `port_in_redirect off` and a correct `server_name`.

Examples
absolute_redirect off;
port_in_redirect off;
server_name_in_redirect off;
merge_slashes off;

By default nginx collapses `//` in a URI to `/`. Turn merging off when a backend genuinely needs the double slash preserved.

Common pitfall: Turning it off changes how location prefixes match — test your routing afterward. Most apps want the default `on`.

Examples
merge_slashes off;
Location matching (12)
location / { ... }

Prefix match — matches any URI that starts with the given string. The default catch-all.

Examples
location / {
    try_files $uri $uri/ /index.html;
}
location = /healthz { ... }

Exact match (=). Highest priority — when the URI matches exactly, nginx stops searching immediately.

Common pitfall: Use `=` on hot endpoints like `/healthz` or `/` to skip the whole regex-location scan and save real CPU on busy hosts.

Examples
location = /healthz {
    return 200 "ok";
    access_log off;
}
location = / {
    return 301 /home;
}
location ^~ /static/ { ... }

Prefix match with preferential override (^~). If this prefix matches, nginx will NOT try any regex locations.

Examples
location ^~ /static/ {
    alias /var/www/site/static/;
    expires 30d;
}
location ~ \.php$ { ... }

Case-SENSITIVE regex match (~). For case-insensitive use `~*` instead.

Examples
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
}
location ~* \.(jpg|jpeg|png|gif|webp)$ { ... }

Case-INSENSITIVE regex match (~*). Standard pattern for image caching.

Examples
location ~* \.(jpg|jpeg|png|gif|webp|svg|ico)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}
location matching priority

Order: 1) exact `=` 2) longest `^~` prefix 3) first matching regex (~ / ~*) in file order 4) longest plain prefix.

Common pitfall: Regex order matters — first match wins, not "best" or "longest". Reorder regex locations to fix surprises.

Examples
# Tested order:
location = / { ... }              # 1 exact
location ^~ /static/ { ... }       # 2 ^~ prefix
location ~* \.(jpg|png)$ { ... }   # 3 regex (first match)
location / { ... }                 # 4 plain prefix (fallback)
named location @fallback

A location named with @ is reachable only via internal redirects (try_files last arg, error_page). It never matches an external URI directly.

Examples
location / {
    try_files $uri $uri/ @app;
}

location @app {
    proxy_pass http://backend;
}
internal;

Mark a location as reachable only from inside nginx (internal redirects, X-Accel-Redirect, error_page). External requests get 404.

Examples
location /protected/ {
    internal;
    alias /var/www/private/;
}
location /download/ {
    internal;
    root /data;
}
X-Accel-Redirect (protected downloads)

Let your app authorize a download, then hand the file off to nginx via an internal location. nginx streams it with sendfile; the app never touches the bytes.

Common pitfall: The target location MUST be marked `internal;` or anyone could fetch the file directly and bypass your auth check.

Examples
# nginx side
location /protected-files/ {
    internal;
    alias /var/secure/;
}

# app returns header after auth:
#   X-Accel-Redirect: /protected-files/report.pdf
deny / allow (IP access control)

Allow or deny by client IP / CIDR. Rules are checked top to bottom; the first match wins. End an allow-list with `deny all`.

Common pitfall: Behind a proxy, `$remote_addr` is the proxy IP — these rules see the wrong address. Pair with the realip module so they match the true client.

Examples
location /admin/ {
    allow 10.0.0.0/8;
    allow 192.168.1.0/24;
    deny all;
}
auth_basic "Restricted";

HTTP Basic auth. Point auth_basic_user_file at an htpasswd file. Simple gate for staging or an admin path.

Common pitfall: Basic auth sends the password base64-encoded, not encrypted — only use it over HTTPS. Generate the file with `htpasswd -c /etc/nginx/.htpasswd user`.

Examples
location /admin/ {
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
}
auth_request /auth;

Delegate authorization to a subrequest. nginx calls the auth endpoint first; 2xx lets the request through, 401/403 blocks it.

Common pitfall: Needs `--with-http_auth_request_module` (built in on most distro packages). The auth endpoint should be `internal;` and return only a status, no body.

Examples
location /private/ {
    auth_request /auth;
    proxy_pass http://backend;
}

location = /auth {
    internal;
    proxy_pass http://auth-service/verify;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}
Reverse proxy (16)
proxy_pass http://backend;

Forward the request to an upstream backend. Can be a URL or an upstream name defined with `upstream { ... }`.

Common pitfall: Trailing slash matters: `proxy_pass http://backend/` rewrites the URI, `proxy_pass http://backend` keeps it. Get them mixed up and routing breaks silently.

Examples
location /api/ {
    proxy_pass http://127.0.0.1:3000/;
}
location / {
    proxy_pass http://backend;
}
upstream backend { ... }

Define a named pool of backend servers. Use the name with `proxy_pass http://<name>`.

Examples
upstream backend {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    keepalive 32;
}
upstream backend {
    least_conn;
    server app1.internal:8080 weight=3;
    server app2.internal:8080;
}
proxy_set_header Host $host;

Forward the original Host header to the backend. Without this, your app sees the upstream name and routing/cookies break.

Common pitfall: After ANY `proxy_set_header` directive, the default headers (Host + Connection) are REPLACED — re-set Host explicitly when you customize headers.

Examples
proxy_set_header Host $host;
proxy_set_header Host $http_host;  # includes :port
proxy_set_header X-Real-IP $remote_addr;

Forward the real client IP. Without it the backend logs nginx as the client.

Examples
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;
proxy_http_version 1.1;

Use HTTP/1.1 to the upstream. Required for keep-alive AND for WebSocket upgrades.

Common pitfall: Default is HTTP/1.0 — without `proxy_http_version 1.1` upstream keep-alive does nothing and a fresh TCP connection is opened per request.

Examples
proxy_http_version 1.1;
proxy_http_version 1.1;
proxy_set_header Connection "";
WebSocket upgrade

Forward the Upgrade + Connection headers so an HTTP request becomes a WebSocket.

Common pitfall: Forget either header and the WS handshake just hangs at "Pending" in the browser DevTools network tab.

Examples
location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;
}
proxy_read_timeout 60s;

Max time between two read operations from the upstream (default 60s). Bump it for slow APIs or WebSockets.

Common pitfall: WebSockets idle 60s and the connection gets killed with a 504. Set to 3600s or higher for WS endpoints.

Examples
proxy_read_timeout 60s;
proxy_read_timeout 3600s;  # for WebSockets
proxy_connect_timeout 5s;

How long nginx waits to establish the TCP connection to the upstream. Default 60s — usually too forgiving.

Examples
proxy_connect_timeout 5s;
proxy_connect_timeout 2s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
proxy_buffering on;

Buffer the upstream response before sending to the client. On = throughput. Off = streaming.

Common pitfall: For SSE (Server-Sent Events) or live log tailing, set `proxy_buffering off` or events queue up.

Examples
proxy_buffering on;
location /events {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
}
proxy_redirect off;

Disable nginx rewriting Location / Refresh headers from the upstream. Use `default` to auto-rewrite based on proxy_pass.

Examples
proxy_redirect off;
proxy_redirect default;
proxy_set_header Connection "";

Clear the Connection header so upstream keep-alive actually works. Required alongside proxy_http_version 1.1 for a pooled upstream.

Common pitfall: For a keep-alive upstream pool set Connection to "" (empty), NOT "keep-alive". For a WebSocket location set it to "upgrade" instead. Mixing these up breaks one or the other.

Examples
upstream api { server 127.0.0.1:3000; keepalive 32; }

location /api/ {
    proxy_pass http://api;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}
proxy_next_upstream error timeout;

Conditions under which nginx retries the request against the next upstream server. Lets a load balancer skip a dead backend.

Common pitfall: Do NOT include `non_idempotent` blindly — retrying a failed POST can double-charge or double-write. By default nginx already refuses to retry non-idempotent requests.

Examples
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
proxy_cache_bypass $http_pragma;

Define when to bypass the cache and go to the upstream (response may still be cached). Common: bypass on a "no-cache" hint.

Examples
proxy_cache_bypass $http_pragma $http_authorization;
proxy_no_cache $http_authorization;
proxy_ssl_server_name on;

Send SNI to an HTTPS upstream. Required when the backend serves multiple certs behind one IP and picks by SNI.

Common pitfall: Off by default — without it an HTTPS upstream on a shared host can return the wrong cert or reject the handshake. Set `proxy_ssl_name` if the SNI must differ from the proxy_pass host.

Examples
location / {
    proxy_pass https://backend.example.com;
    proxy_ssl_server_name on;
    proxy_ssl_protocols TLSv1.2 TLSv1.3;
}
proxy_pass_request_headers off;

Stop forwarding the client request headers to the upstream. Mostly used on auth subrequests to keep them lean.

Examples
location = /auth {
    internal;
    proxy_pass http://auth/verify;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}
proxy_intercept_errors on;

Let nginx replace upstream error responses (≥300) with its own error_page handlers, so a backend 500 can show your branded page.

Examples
location / {
    proxy_pass http://backend;
    proxy_intercept_errors on;
    error_page 500 502 503 504 /50x.html;
}
Static files (10)
root vs alias

root: nginx appends the URI to root. alias: nginx REPLACES the matched location with alias. Two different mechanics.

Common pitfall: For `/static/` → `/var/www/files/`, alias is right; root would look for `/var/www/files/static/...` and 404.

Examples
# root style — keeps URI prefix
location /static/ {
    root /var/www;  # serves /var/www/static/...
}
# alias style — strips URI prefix
location /static/ {
    alias /var/www/files/;  # serves /var/www/files/...
}
try_files $uri $uri/ /index.html;

Try each path in order — first one that exists is served. The classic SPA fallback pattern.

Common pitfall: The LAST argument is either a URI (internal redirect) or `=code` (return status). A bare filename will be served literally — usually not what you want.

Examples
# SPA fallback
location / {
    try_files $uri $uri/ /index.html;
}
# 404 if nothing matches
location / {
    try_files $uri =404;
}
# PHP front controller
location / {
    try_files $uri $uri/ /index.php?$query_string;
}
autoindex on;

Show a directory listing when no index file matches. Off by default — and that is correct for production.

Common pitfall: Turn this on globally and you leak file structure. Restrict to a specific location, or leave off.

Examples
location /downloads/ {
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;
}
expires 30d;

Set the Expires + Cache-Control headers on responses. Standard for hashed asset files.

Examples
location ~* \.(js|css|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
expires -1;  # explicitly disable caching
error_page 404 /404.html;

Serve a custom page for a given status code. Use `=200` to also change the returned status, or a named location to proxy the handler.

Common pitfall: The custom 404 location should usually itself be `internal;` so it cannot be requested directly. Add `error_page 404 = /404.html;` (with `=`) to make it return 404, not 200.

Examples
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html { root /usr/share/nginx/html; internal; }
add_header X-Content-Type-Options nosniff;

Stop browsers from MIME-sniffing a response away from its declared Content-Type. A one-line defense against some XSS vectors.

Common pitfall: A bare `add_header` inside a location is dropped the moment that location sets ANY other add_header. Use `always` and remember headers do not inherit into a child block that adds its own.

Examples
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy ...

Send a CSP to control which sources of script, style, and media the browser will load. The strongest header-level XSS mitigation.

Common pitfall: Roll it out with `Content-Security-Policy-Report-Only` first and watch the violation reports — a too-strict CSP silently breaks your own site.

Examples
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; object-src 'none'" always;
gzip_static on;

Serve a pre-compressed `file.gz` sitting next to the original instead of compressing on the fly. Zero CPU per request for static assets.

Common pitfall: You must generate the `.gz` files at build time (e.g. `gzip -k -9 dist/**/*.js`). nginx does not create them — it only serves them when they already exist.

Examples
location ~* \.(js|css|svg)$ {
    gzip_static on;
    expires 1y;
    add_header Cache-Control "public, immutable";
}
open_file_cache max=10000 inactive=60s;

Cache open file descriptors, sizes, and existence checks. Big win for sites serving many small static files repeatedly.

Examples
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
add_header Accept-Ranges bytes;

Advertise byte-range support so clients can resume downloads and stream video with seeking. nginx enables ranges for static files automatically.

Common pitfall: Range support is disabled when gzip compresses the response on the fly — pre-compressed `gzip_static` files keep ranges working for large downloads.

Examples
location /video/ {
    root /var/media;
    add_header Accept-Ranges bytes;
    mp4;
}
Redirect / rewrite (7)
return 301 https://$host$request_uri;

Permanent redirect to HTTPS. Cleanest, fastest way — no rewrite engine, no regex.

Common pitfall: Use 301 (permanent) for production redirects so browsers + CDNs cache them. Use 302 only when the redirect target is genuinely temporary.

Examples
# Force HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
# Redirect old path
location = /old { return 301 /new; }
rewrite ^/old/(.*)$ /new/$1 permanent;

Rewrite a URI. `permanent` = 301, `redirect` = 302, `last` = re-search locations, `break` = stop rewriting.

Common pitfall: For a simple redirect prefer `return 301 ...` — it is faster and clearer than `rewrite`. Reach for rewrite only when you need regex captures.

Examples
rewrite ^/old/(.*)$ /new/$1 permanent;
rewrite ^/blog/(.*)\.html$ /posts/$1 last;
if ($scheme = "http")

Conditional block. Officially "if is evil" — works inside `location` only and has surprising scope rules.

Common pitfall: Inside `if` blocks, only `return` and `rewrite ... last` are safe. Other directives can silently misbehave. Prefer a separate server block over `if`.

Examples
# Force HTTPS via if (only when a separate server block is awkward)
if ($scheme = "http") {
    return 301 https://$host$request_uri;
}
return 410;

Tell crawlers a URL is gone for good (better than 404 for deleted pages).

Examples
location = /removed-feature { return 410; }
return 302 (temporary redirect)

Temporary redirect. Browsers and CDNs do NOT cache it, so the move can be reverted. Use 307 to also preserve the request method/body.

Common pitfall: A 302 on a POST silently turns it into a GET in many clients. If you must redirect a POST and keep it a POST, use 307 instead.

Examples
location = /beta { return 302 /coming-soon; }
return 307 https://$host$request_uri;
www to non-www redirect

Canonicalize the host so search engines index one URL. A dedicated server block for the www name is cleaner than an `if`.

Common pitfall: Pick ONE canonical host and stick with it everywhere (sitemap, canonical tag, redirect). Redirecting both ways creates a loop.

Examples
# www → root
server {
    listen 80;
    server_name www.example.com;
    return 301 $scheme://example.com$request_uri;
}
map for bulk redirects

Drive hundreds of old-to-new redirects from a single `map`. Faster and far more readable than a wall of `if` or `location` rules.

Common pitfall: `map` blocks live in the `http {}` context, not inside `server` or `location`. The default value (the `default` line) is what unmatched URIs fall back to.

Examples
# in http { }
map $request_uri $redirect_to {
    default            "";
    /old-page          /new-page;
    /legacy/docs       /docs;
}

# in server { }
if ($redirect_to) {
    return 301 $redirect_to;
}
HTTPS / SSL (12)
ssl_certificate / ssl_certificate_key

Path to the fullchain cert and private key. Required for any TLS listener.

Common pitfall: Use FULLCHAIN (cert + intermediates) not just the leaf cert — otherwise mobile clients and older browsers fail to verify.

Examples
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;

Allowed TLS versions. TLS 1.0 / 1.1 are deprecated and broken in all modern browsers — never enable them.

Examples
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers <modern-cipher-list>

Allowed cipher suites. Copy the latest "Modern" or "Intermediate" list from ssl-config.mozilla.org — do not invent your own.

Examples
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
Let's Encrypt with certbot

Free TLS certs with auto-renew. `certbot --nginx` edits your nginx config in place. Renewal is via a systemd timer.

Common pitfall: Renewal needs port 80 reachable from the public internet. Behind a firewall use DNS-01 (`--manual --preferred-challenges dns`).

Examples
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
sudo systemctl list-timers | grep certbot
add_header Strict-Transport-Security ...

HSTS — tells browsers to remember "use HTTPS only for this domain" for the given max-age.

Common pitfall: Once a browser receives HSTS with `includeSubDomains` + `preload`, you CANNOT downgrade for that period. Roll it out short max-age first (e.g. 1 day).

Examples
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
ssl_stapling on;

OCSP stapling — nginx fetches the OCSP response and attaches it to the handshake, saving the client a round-trip.

Examples
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
ssl_session_cache shared:SSL:10m;

Shared TLS session cache across workers. 10MB ≈ 40,000 sessions. Big speed win on busy sites.

Examples
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_dhparam /etc/nginx/dhparam.pem;

Custom Diffie-Hellman params for DHE cipher suites. Generate a 2048-bit (or larger) file so you are not using nginx’s weak built-in default.

Common pitfall: Generating 4096-bit dhparam can take minutes and adds handshake cost for little real gain — 2048-bit is the common, well-supported choice. Pure TLS 1.3 / ECDHE setups do not use it at all.

Examples
openssl dhparam -out /etc/nginx/dhparam.pem 2048
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_early_data on;

Enable TLS 1.3 0-RTT so a returning client can send the first request in the very first packet. Cuts a round-trip on reconnects.

Common pitfall: 0-RTT data is replayable by an attacker. Only allow it for idempotent requests, and check `$ssl_early_data` so you can refuse early-data POSTs at the app.

Examples
ssl_early_data on;
proxy_set_header Early-Data $ssl_early_data;
ssl_trusted_certificate ...

Trust store used to verify the OCSP responder and (with ssl_verify_client) client certs. Needed for stapling verification on some chains.

Examples
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
ssl_verify_client on; (mTLS)

Require a client certificate (mutual TLS). nginx rejects the handshake unless the client presents a cert signed by `ssl_client_certificate`.

Common pitfall: Use `optional` instead of `on` if some paths are public — then gate the protected location on `$ssl_client_verify = SUCCESS` yourself.

Examples
ssl_client_certificate /etc/nginx/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
if ($ssl_client_verify != SUCCESS) { return 403; }
http2 on;

The nginx ≥ 1.25.1 way to enable HTTP/2, separate from the listen line. Replaces the deprecated `listen ... http2` flag.

Examples
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
gzip / brotli / cache (9)
gzip on;

Enable on-the-fly gzip compression for responses. Standard win for text content.

Common pitfall: Do NOT gzip already-compressed types (jpg, png, mp4, zip). Use `gzip_types` to explicitly list text MIME types.

Examples
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
brotli on;

Brotli compression (ngx_brotli module). ~15-25% smaller than gzip on text. Requires a module compiled in.

Common pitfall: `brotli` is NOT in stock nginx — `nginx -V | grep brotli` first. On Ubuntu use `libnginx-mod-brotli` or rebuild.

Examples
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
proxy_cache_path /var/cache/nginx ...

Define an on-disk cache zone for upstream responses. Then enable per-location with `proxy_cache <zone>`.

Examples
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m max_size=1g inactive=60m use_temp_path=off;

location / {
    proxy_cache mycache;
    proxy_cache_valid 200 302 10m;
    proxy_cache_valid 404 1m;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    add_header X-Cache-Status $upstream_cache_status;
    proxy_pass http://backend;
}
add_header Cache-Control ...

Cache directives for the BROWSER. `public, immutable` for hashed assets; `no-store` for HTML; `must-revalidate` for safe staleness.

Examples
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Cache-Control "no-store" always;
proxy_cache_key $scheme$host$uri$is_args$args;

Define exactly what makes two requests "the same" for the cache. The default ignores scheme; add it to avoid serving an http body for https.

Common pitfall: Leaving a per-user cookie or auth token out of the key is what lets one user’s private page get served to everyone. If responses vary by user, do not cache them.

Examples
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_lock on;

When many requests miss the cache for the same key at once, let only one go to the upstream and make the rest wait for it. Stops a cache stampede.

Examples
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale updating;
proxy_cache_min_uses 3;

Only start caching a response after it has been requested this many times. Keeps one-hit URLs from filling the cache.

Examples
proxy_cache_min_uses 3;
proxy_cache_valid 200 10m;
gzip_proxied any;

Control whether nginx gzips responses that came from an upstream. Default `off` means proxied responses are never compressed — usually you want `any`.

Common pitfall: If gzip works for static files but not for proxied pages, this is almost always why — the default `gzip_proxied off` skips them.

Examples
gzip on;
gzip_proxied any;
gzip_vary on;
add_header X-Cache-Status $upstream_cache_status;

Expose whether each response was a HIT, MISS, BYPASS, EXPIRED, or STALE. The fastest way to confirm your cache config actually works.

Examples
add_header X-Cache-Status $upstream_cache_status always;
# then:
curl -sI https://example.com/ | grep -i x-cache-status
Rate limit (7)
limit_req_zone $binary_remote_addr ...

Define a per-IP request-rate zone. Then enforce it in a location with `limit_req zone=<name> burst=<n>`.

Common pitfall: Use `$binary_remote_addr` not `$remote_addr` — 4 bytes vs ~15 bytes, so the zone holds 4x more clients in the same memory.

Examples
# Define in http { }
limit_req_zone $binary_remote_addr zone=loginlimit:10m rate=5r/m;

# Apply in location
location /login {
    limit_req zone=loginlimit burst=10 nodelay;
    proxy_pass http://backend;
}
limit_conn_zone $binary_remote_addr ...

Define a per-IP concurrent-connection zone. Enforce with `limit_conn <name> <n>`.

Examples
limit_conn_zone $binary_remote_addr zone=perip:10m;

server {
    limit_conn perip 20;
}
limit_req_status 429;

HTTP status returned when the rate limit is exceeded. Default 503; 429 is the correct semantic.

Examples
limit_req_status 429;
limit_conn_status 429;
burst= and nodelay explained

burst lets a short spike queue up beyond the rate; nodelay forwards those queued requests immediately instead of spacing them out. Together: smooth average, instant for bursts.

Common pitfall: Without `nodelay`, a `rate=10r/s burst=20` releases queued requests at one per 100ms, so a legit burst feels laggy. With `nodelay` they go through at once but the rate cap still holds over time.

Examples
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://backend;
}
limit_req_log_level warn;

Severity used when a request is delayed or rejected by a rate limit. Lower it to `notice` to stop limiter noise from dominating error.log.

Examples
limit_req_log_level notice;
limit_conn_log_level notice;
limit_rate 512k;

Cap the per-connection response bandwidth (bytes/s). Use `limit_rate_after` to send the first chunk fast, then throttle the rest of a large download.

Examples
location /downloads/ {
    limit_rate_after 10m;
    limit_rate 512k;
}
geo + map whitelist for limits

Exempt trusted IPs from rate limiting: a `geo` block sets a flag, a `map` turns the limiter key off for them so their requests never count.

Examples
geo $limit_exempt {
    default        0;
    10.0.0.0/8     1;
    192.168.0.0/16 1;
}
map $limit_exempt $limit_key {
    0 $binary_remote_addr;
    1 "";
}
limit_req_zone $limit_key zone=api:10m rate=10r/s;
Logging (8)
access_log /var/log/nginx/access.log main;

Where + what to log per request. `main` is the default format name. Set to `off` for hot endpoints like /healthz.

Examples
access_log /var/log/nginx/access.log main;
access_log off;
access_log /var/log/nginx/api.log combined buffer=64k flush=5s;
error_log /var/log/nginx/error.log warn;

Where + at what severity to log nginx-internal errors. Levels: debug, info, notice, warn, error, crit, alert, emerg.

Common pitfall: Setting `debug` floods the log. Use it only when actively debugging, with `error_log /tmp/debug.log debug;` so it does not hit the main log.

Examples
error_log /var/log/nginx/error.log warn;
error_log /var/log/nginx/error.log error;
log_format main ...

Define a named log line format. Lots of useful variables: $request_time, $upstream_response_time, $body_bytes_sent.

Examples
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for" '
                'rt=$request_time urt=$upstream_response_time';
log_format json escape=json '{"time":"$time_iso8601","remote":"$remote_addr","req":"$request","status":$status,"rt":$request_time,"urt":"$upstream_response_time"}';
tail -F /var/log/nginx/access.log

Follow the access log in real time. -F (capital) follows across log-rotate, which is what you actually want.

Examples
sudo tail -F /var/log/nginx/access.log
sudo tail -F /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn
access_log syslog:server=...

Ship access logs straight to a syslog server (local or remote) instead of a file. Lets a log collector ingest nginx without tailing files.

Examples
access_log syslog:server=127.0.0.1:514,tag=nginx,severity=info main;
error_log syslog:server=logs.internal:514 warn;
map to skip health-check logging

Use a `map` on $request_uri or $http_user_agent to set a flag, then `access_log ... if=$flag` so probes and monitors never clutter the log.

Examples
map $request_uri $loggable {
    default        1;
    /healthz       0;
    /metrics       0;
}

access_log /var/log/nginx/access.log main if=$loggable;
open_log_file_cache max=1000;

Cache open log-file descriptors when you split logs across many files (e.g. per-vhost). Saves an open/close per request.

Examples
open_log_file_cache max=1000 inactive=20s valid=1m min_uses=2;
GoAccess on the access log

Turn the access log into a live HTML dashboard. GoAccess parses nginx’s combined/custom format and shows top URLs, status codes, and bandwidth.

Common pitfall: If you use a custom log_format, pass a matching `--log-format` to GoAccess or it parses nothing. The combined format works out of the box.

Examples
goaccess /var/log/nginx/access.log -o report.html --log-format=COMBINED --real-time-html
zcat /var/log/nginx/access.log.*.gz | goaccess --log-format=COMBINED -o report.html
Common errors (15)
502 Bad Gateway

nginx connected to the upstream but got an invalid / empty / closed response. Almost always the BACKEND is dead, crashed, or wrong port.

Common pitfall: Check the backend is alive first: `curl http://127.0.0.1:3000`. Then nginx error.log — you usually see "connect() failed" or "upstream prematurely closed connection".

Examples
curl -v http://127.0.0.1:3000
sudo tail -f /var/log/nginx/error.log
sudo journalctl -u my-backend -n 200
504 Gateway Timeout

Upstream is alive but did not answer within proxy_read_timeout. Either bump the timeout or fix the slow query.

Common pitfall: WebSockets and long-poll endpoints constantly hit this. Set `proxy_read_timeout 3600s` only on those specific locations, not globally.

Examples
proxy_read_timeout 3600s;
proxy_send_timeout 600s;
SSL handshake failed

TLS negotiation failed. Most common causes: wrong cert/key pair, missing intermediates, unsupported protocol or cipher, SNI mismatch.

Common pitfall: Always test with `openssl s_client -connect host:443 -servername host` (the -servername sends SNI). Browsers cache TLS failures for a long time.

Examples
openssl s_client -connect example.com:443 -servername example.com
openssl x509 -in fullchain.pem -noout -dates -subject -issuer
sudo nginx -t
permission denied (static files)

Worker user cannot read your files. Check ownership against the `user` directive AND that every parent directory is `x`-able.

Common pitfall: Every parent directory needs the `x` bit for nginx, not just the file with `r`. `chmod -R 755 /var/www/site` fixes it in one shot.

Examples
ls -ld /var/www /var/www/site /var/www/site/index.html
sudo chown -R www-data:www-data /var/www/site
sudo chmod -R 755 /var/www/site
address already in use (port 80/443)

Another process holds the port. Apache, an old nginx, a stray docker container — find it and stop it.

Examples
sudo ss -tlnp | grep -E ":80|:443"
sudo lsof -i :80 -i :443
sudo systemctl stop apache2
conflicting server_name warning

Two server blocks declare the same server_name on the same listen. nginx picks ONE silently — your other site goes dark.

Common pitfall: Run `nginx -T | grep -E "(server_name|listen)"` after every deploy to spot duplicates before they bite.

Examples
sudo nginx -T 2>&1 | grep -E "server_name|listen" | sort | uniq -d
worker connections are not enough

You hit worker_connections × worker_processes. Bump worker_connections AND worker_rlimit_nofile AND systemd LimitNOFILE.

Examples
worker_rlimit_nofile 65535;
events { worker_connections 16384; }
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65535
client intended to send too large body

Uploads larger than `client_max_body_size` get 413. Set it on the http, server, or location level.

Examples
# In http or server
client_max_body_size 100m;
# Only the upload endpoint
location /upload {
    client_max_body_size 1g;
    proxy_pass http://backend;
}
upstream sent too big header

The upstream response headers overflow nginx’s proxy buffers, giving a 502. Common with big Set-Cookie or auth headers.

Common pitfall: Raise `proxy_buffer_size` (the buffer for the response header) AND `proxy_buffers`. The header alone is limited by proxy_buffer_size, not the whole proxy_buffers pool.

Examples
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
rewrite or internal redirection cycle

try_files or rewrite keeps pointing back at a URI that re-enters the same location, and nginx aborts the loop with a 500.

Common pitfall: Classic cause: `try_files $uri $uri/ /index.php` inside `location ~ \.php$` — the fallback re-matches the php location forever. Move the fallback to `location /`.

Examples
sudo tail -f /var/log/nginx/error.log | grep "rewrite or internal redirection cycle"
no live upstreams

Every server in the upstream pool is marked down (too many failed health checks), so nginx returns 502 with "no live upstreams" in error.log.

Common pitfall: Aggressive `max_fails` plus a brief backend blip can take the whole pool down at once. Tune `max_fails` / `fail_timeout`, and keep a `backup` server.

Examples
upstream app {
    server a:8080 max_fails=3 fail_timeout=15s;
    server b:8080 max_fails=3 fail_timeout=15s;
    server c:8080 backup;
}
duplicate default_server

Two server blocks both claim `default_server` on the same listen address, so `nginx -t` refuses to load the config.

Common pitfall: Search all included files: `grep -rn default_server /etc/nginx/`. A leftover `00-default.conf` is the usual culprit.

Examples
sudo grep -rn "default_server" /etc/nginx/
sudo nginx -t
redirect loop behind a proxy/CDN

Your HTTP→HTTPS redirect fires forever because the CDN talks to nginx over HTTP, so $scheme is always "http" even on a real HTTPS request.

Common pitfall: Redirect on `$http_x_forwarded_proto` instead of `$scheme`, or set `proxy_protocol`/realip so nginx knows the original scheme. Never trust $scheme behind an offloading proxy.

Examples
if ($http_x_forwarded_proto = "http") {
    return 301 https://$host$request_uri;
}
too many open files

nginx logs "socket() failed (24: Too many open files)" because the file-descriptor limit is too low for the connection volume.

Common pitfall: Raise THREE things together: `worker_rlimit_nofile`, systemd `LimitNOFILE`, and the OS `nofile` ulimit. Missing any one and the real ceiling is the lowest of them.

Examples
worker_rlimit_nofile 100000;
# /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=100000
cat /proc/$(pgrep -o nginx)/limits | grep "open files"
add_header lost in a child location

Headers set with add_header at the server level vanish inside any location that adds its OWN add_header. nginx replaces, it does not merge.

Common pitfall: Re-declare every header you need in each location that touches headers, or centralize them with the headers-more module’s `more_set_headers`, which does inherit.

Examples
# server-level header is dropped here:
location /api/ {
    add_header X-Api "1" always;          # this replaces ALL inherited add_headers
    add_header X-Frame-Options SAMEORIGIN always;  # must repeat
}
Full templates (11)
Template: static site

Bare-bones static site server block. Serves /var/www/site, gzip on, long-cache hashed assets, SPA-friendly fallback.

Examples
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/site;
    index index.html;

    gzip on;
    gzip_types text/plain text/css application/json application/javascript image/svg+xml;

    location ~* \.(js|css|woff2|png|jpg|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}
Template: reverse proxy to Node

Forward / to a Node app on 127.0.0.1:3000 with the standard forwarded headers and WebSocket support.

Examples
upstream node_app {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_read_timeout 60s;
        proxy_buffering on;
    }
}
Template: HTTPS with Let's Encrypt

Full HTTPS server with HTTP→HTTPS redirect, modern TLS, HSTS, OCSP stapling. Drop-in template.

Examples
# HTTP → HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# HTTPS
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.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_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

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

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

    location / {
        try_files $uri $uri/ /index.html;
    }
}
Template: load balancer

Round-robin (default) or least-conn load balance across three backends, with health-aware fail_timeout.

Examples
upstream backend {
    least_conn;
    server app1.internal:8080 max_fails=3 fail_timeout=30s;
    server app2.internal:8080 max_fails=3 fail_timeout=30s;
    server app3.internal:8080 max_fails=3 fail_timeout=30s backup;
    keepalive 64;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_next_upstream error timeout http_502 http_503 http_504;
    }
}
Template: PHP-FPM

Pass .php files to PHP-FPM via a unix socket. Standard WordPress / Laravel-ready front controller.

Examples
server {
    listen 80;
    server_name example.com;
    root /var/www/site/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht { deny all; }
}
Template: /healthz endpoint

Cheap exact-match health endpoint with no logging. Friendly to load balancers, Kubernetes probes, uptime monitors.

Examples
server {
    listen 80 default_server;

    location = /healthz {
        access_log off;
        return 200 "ok\n";
        add_header Content-Type text/plain;
    }
}
Template: maintenance / 503 page

Serve a friendly "be right back" page with HTTP 503 and a Retry-After header. Flip a single file to toggle maintenance mode on or off.

Examples
server {
    listen 80;
    server_name example.com;
    root /var/www/site;

    # If the flag file exists, everything returns the maintenance page.
    if (-f $document_root/maintenance.on) {
        return 503;
    }

    error_page 503 @maintenance;
    location @maintenance {
        root /var/www/site;
        rewrite ^(.*)$ /maintenance.html break;
        add_header Retry-After 3600 always;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}
Template: CORS preflight handling

Answer OPTIONS preflight requests at nginx and add CORS headers to API responses, so a browser front-end on another origin can call the API.

Examples
server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin "https://app.example.com" always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
            add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
            add_header Access-Control-Max-Age 86400 always;
            add_header Content-Length 0;
            return 204;
        }

        add_header Access-Control-Allow-Origin "https://app.example.com" always;
        proxy_pass http://backend;
    }
}
Template: gRPC proxy

Reverse-proxy gRPC over HTTP/2 with grpc_pass. nginx terminates TLS for the client and forwards to a plaintext or TLS gRPC backend.

Common pitfall: gRPC needs HTTP/2 end to end. Use `grpc_pass` (not proxy_pass) and `grpc_pass grpcs://` for a TLS backend. The listen side must have `http2 on;`.

Examples
server {
    listen 443 ssl;
    http2 on;
    server_name grpc.example.com;

    ssl_certificate     /etc/letsencrypt/live/grpc.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grpc.example.com/privkey.pem;

    location / {
        grpc_pass grpc://127.0.0.1:50051;
        grpc_set_header Host $host;
    }
}
Template: micro-cache for dynamic pages

Cache dynamic HTML for just 1 second. Under a traffic spike this collapses thousands of identical requests into one upstream hit, with stale-while-revalidate.

Common pitfall: Never micro-cache responses that vary per logged-in user. Add `proxy_cache_bypass`/`proxy_no_cache` on the auth cookie so authenticated requests skip the shared cache.

Examples
proxy_cache_path /var/cache/nginx/micro levels=1:2 keys_zone=micro:10m max_size=256m inactive=60s use_temp_path=off;

map $http_cookie $no_cache {
    default       0;
    ~session_id   1;
}

server {
    location / {
        proxy_cache micro;
        proxy_cache_valid 200 1s;
        proxy_cache_use_stale updating error timeout;
        proxy_cache_lock on;
        proxy_cache_bypass $no_cache;
        proxy_no_cache $no_cache;
        add_header X-Cache-Status $upstream_cache_status always;
        proxy_pass http://backend;
    }
}
Template: subdomain wildcard vhost

Map each subdomain to its own document root automatically with a captured server_name and a variable in root. Add one folder, get one site.

Common pitfall: Wildcard server blocks need a wildcard DNS record (`*.example.com`) and, for HTTPS, a wildcard TLS cert. Validate the captured `$sub` if you do not trust arbitrary subdomains.

Examples
server {
    listen 80;
    server_name ~^(?<sub>[a-z0-9-]+)\.example\.com$;
    root /var/www/tenants/$sub;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

What this tool does

Searchable Nginx cheat sheet with 80+ directives, CLI flags, and full templates ops engineers actually paste into production. Thirteen categories: CLI commands (nginx -t / -s reload / -s quit / -s reopen / -V / -T), core directives (worker_processes, events, http, include, sendfile, keepalive_timeout, client_max_body_size, worker_rlimit_nofile), server block (listen 80 / 443 ssl http2, server_name wildcard + regex, root, index, return 444 for unknown Host), location matching with the full priority order (= exact, ^~ preferential prefix, ~ / ~* regex, plain prefix), reverse proxy (proxy_pass trailing-slash gotcha, upstream pools, proxy_set_header Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Proto, WebSocket Upgrade + Connection upgrade, proxy_http_version 1.1, proxy_read_timeout, proxy_buffering off for SSE), static files (root vs alias trap, try_files for SPA fallback, autoindex, expires + immutable), redirects (return 301 / 302 / 410, rewrite last vs break, "if is evil"), HTTPS / SSL (ssl_certificate fullchain, TLSv1.2/1.3, Let&#39;s Encrypt + certbot --nginx, HSTS preload, OCSP stapling, session cache), caching (gzip + gzip_types, ngx_brotli, proxy_cache_path stale-while-revalidate, Cache-Control), rate limit (limit_req_zone with $binary_remote_addr, limit_conn_zone, limit_req_status 429), logging (access_log buffer + flush, error_log levels, JSON log_format), the seven errors that ruin your day (502 / 504 / SSL handshake / permission denied / port in use / conflicting server_name / worker connections), and six copy-ready full templates (static site, reverse proxy to Node, HTTPS with Let&#39;s Encrypt, load balancer with fail_timeout, PHP-FPM, /healthz). Every entry has the directive, bilingual EN/ZH description, the trap people hit, and one to four copy-ready examples. Search across directive + description + pitfall + example, category chips, one-click copy. Fully client-side, no config upload. Pair with Docker, kubectl, Git Cheatsheets.

Tool details

Input
Files + Text
The page exposes text boxes, numeric controls, file pickers, or structured inputs depending on the tool.
Output
Live result + Copy + Preview
The result area focuses on usable output, with copy, download, or preview actions when supported.
Privacy
Browser-side processing
The main tool logic does not call an external API, so inputs normally stay in the current tab.
Save / share
No account required
Open the page and use it; whether results survive refresh depends on the tool.
Performance budget
Initial JS <= 30 KB
No WASM budget is declared, keeping the tool quick to open on mobile.
Best fit
Developer & DevOps · Developer
Category and role tags drive related tools, internal links, and quick fit checks.

How to use

  1. 1. Input

    Paste or drop your content into the tool panel.

  2. 2. Process

    Click the button. All processing is local in your browser.

  3. 3. Copy / Download

    Copy the result or download to disk in one click.

How Nginx Cheatsheet fits into your work

Use it in the small gaps between coding, reviewing, debugging, and shipping.

Developer jobs

  • Formatting, validating, shrinking, or inspecting code-adjacent text.
  • Preparing snippets for documentation, tickets, commits, or handoff.
  • Checking a small payload quickly without switching tools.

Developer checks

  • Run irreversible transforms like minify or obfuscate on a copy.
  • Keep secrets out of pasted snippets unless the tool explicitly stays local.
  • Use your normal tests or linter before shipping transformed code.

Good next steps

These links move the current task into a more complete workflow.

  1. 1 Regex Cheatsheet Interactive regex cheat sheet — quick reference for every flavor (JS, Python, PCRE). Open
  2. 2 Docker Cheatsheet Docker command cheat sheet — 80+ commands with real examples, common mistakes, and Compose section. Open
  3. 3 kubectl Cheatsheet kubectl cheat sheet — 100+ Kubernetes commands with real examples, common pitfalls, and YAML snippets. Open

Real-world use cases

  • Stand up a Node app behind nginx on a fresh VPS in 10 minutes

    You just got a $5 droplet and your app listens on 127.0.0.1:3000. Copy the "reverse proxy to Node" template, swap server_name and the upstream port, add the four proxy_set_header lines so the app sees the real client IP instead of 127.0.0.1, run nginx -t, then nginx -s reload. The WebSocket Upgrade block is already in the template, so your socket.io connection works on the first try.

  • Track down a 502 at 2am without guessing

    PagerDuty fires, the site is 502. Search "502" and the entry hands you the exact three checks: curl the upstream directly, tail error.log for "connect() failed (111)" vs "prematurely closed", and the SELinux setsebool line for RHEL boxes. You stop bumping random timeouts (which only fix 504s) and find the dead backend in two minutes instead of twenty.

  • Strip an /api prefix before it hits a backend that does not expect it

    Your Go service mounts routes at /users, /orders, but the frontend calls /api/users. Search "proxy_pass" and the trailing-slash entry spells out that proxy_pass http://backend/ strips the /api/ prefix while no slash keeps it. You add the one slash, confirm with a curl plus the backend access_log, and ship without a 404 hunt.

  • Put a static SPA online with correct deep-link routing

    A React build at /var/www/app 404s when someone refreshes /dashboard. Search "try_files" and the SPA-fallback line gives you try_files $uri $uri/ /index.html; so every unknown path serves the app shell and the router takes over. Add expires + immutable on the hashed JS bundles and your repeat visitors stop re-downloading 800KB on every page load.

Common pitfalls

  • Forgetting the trailing slash on proxy_pass — http://backend/ strips the location prefix, http://backend keeps it, and guessing which is which causes most 404s.

  • Skipping fullchain on ssl_certificate — pointing at the leaf cert alone breaks Android and older clients; always use fullchain.pem, not cert.pem.

  • Reaching for `if` inside a location — `if is evil` has surprising scope rules; use try_files, return, or a separate server block instead of `if ($request_uri ...)`.

Privacy

This cheat sheet is a single static page. Search filters an in-memory array of directives entirely in your browser — your nginx configs are never typed into a server, never uploaded, and never put into the URL. Open DevTools Network while you type and you will see zero requests. Safe behind air-gapped jump hosts and ops bastions.

FAQ

Tool combos

Folks in your role tend to reach for these alongside this tool.

Made by Toolora · 100% client-side · Updated 2026-06-14