Test and Reload
Never restart when a reload will do. Reload keeps existing connections open and falls back to the old config if the new one is broken.
| Command | What it does |
|---|---|
| Test config syntax, prints the failing file and line | |
| Test, then print the full merged config from all includes | |
| Reload config without dropping connections | |
| Same reload, via systemd | |
| Full restart, drops connections; needed for new ports | |
| Is it running, and the last few log lines | |
| Start on boot | |
| Version ( adds modules and compile-time paths) | |
| Stop after current requests finish ( kills immediately) | |
| Start with a specific config file |
The safe edit loop, in one line:
sudo nginx -t && sudo systemctl reload nginxServer Blocks
One block per site (virtual host). nginx picks the block by matching port, then : exact name, longest wildcard, first matching regex, then the .
| Directive | What it does |
|---|---|
| Accept plain HTTP on port 80 | |
| Accept HTTPS (pair with on nginx 1.25+) | |
| Same, on IPv6 | |
| Catch requests that match no | |
| Hostnames this block answers for | |
| Wildcard subdomains | |
| Regex name (starts with ) | |
| Placeholder, used with | |
| Document root for the whole site | |
| Files to try for directory requests |
A minimal static site:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}The nginx config generator builds a full server block from a form.
Location Matching
File order does not decide prefix matches. nginx checks first, then the longest prefix, then regexes in file order; the first regex hit wins, and only if none match does the longest prefix apply.
| Pattern | How it matches |
|---|---|
| Exact URI only, checked first | |
| Prefix; if it is the longest prefix, skip regex checks | |
| Regex, case-sensitive | |
| Regex, case-insensitive | |
| Plain prefix | |
| Prefix that matches everything, the fallback | |
| Serve the file, else the directory, else hand off | |
| Serve the file or return 404 |
Reverse Proxy
forwards requests to an app server. Pass the headers below or the app sees every request as coming from 127.0.0.1 over HTTP.
| Directive | What it does |
|---|---|
| Forward to the app, URI passed through unchanged | |
| Trailing slash: the location prefix is stripped first | |
| Give the app the original hostname | |
| The visitor's IP | |
| The full client IP chain | |
| http or https, so the app builds correct URLs | |
| Needed for websockets and upstream keepalive | |
| Stream responses through (SSE, long polling) |
A complete proxy block, websockets included:
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 "upgrade";
}Upstream
An block names a group of backend servers for . It lives directly inside , next to your server blocks, not inside one.
| Directive | What it does |
|---|---|
| A backend; round-robin between servers is the default | |
| Gets twice the traffic | |
| Used only when the others are down | |
| Temporarily out of rotation | |
| Mark dead after 3 failures, retry after 30s | |
| Send to the backend with fewest active connections | |
| Same client IP always hits the same backend (sticky) | |
| Pool of idle connections to the backends |
upstream app {
least_conn;
server 10.0.0.1:3000;
server 10.0.0.2:3000;
keepalive 32;
}root vs alias
appends the full URI to the path; replaces the location prefix with the path.
location /img/ { root /var/www; }
# /img/a.png -> /var/www/img/a.png
location /img/ { alias /var/www/pics/; }
# /img/a.png -> /var/www/pics/a.png| Rule | Detail |
|---|---|
| Default to | Set it once at level; locations inherit it |
| is location-only | is valid in , , and |
| Match the slashes | If the location ends in , the path must too |
| Never both | An in a location overrides the inherited |
Redirects and Rewrites
For a plain redirect, is simpler and faster than .
| Directive | What it does |
|---|---|
| Permanent redirect, keeps the path and query | |
| Temporary redirect | |
| Answer with a status code, no redirect | |
| Regex redirect with a captured path (301) | |
| Same, temporary (302) | |
| Internal rewrite, re-runs location matching | |
| Internal rewrite, stays in the current location |
The classic www and HTTP cleanup, as separate small server blocks:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
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;
}SSL
| Directive | What it does |
|---|---|
| Serve TLS on this block | |
| Enable HTTP/2 (nginx 1.25.1+; the old parameter is deprecated) | |
| Full chain, not just the leaf cert | |
| Private key | |
| Drop the legacy protocols | |
| Resume sessions, saves handshakes | |
| How long sessions stay resumable | |
| HSTS: browsers stop trying HTTP |
server {
listen 443 ssl;
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_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
}Verify what a live server actually presents with the SSL checker, and inspect a cert file with the certificate decoder.
Gzip and Caching
| Directive | What it does |
|---|---|
| Compress responses ( is always included) | |
| What else to compress | |
| Skip tiny responses, not worth the CPU | |
| 1-9; above 6 costs CPU for little gain | |
| Sets and | |
| Tell browsers not to cache | |
| Full manual control instead of | |
| Define a cache for proxied responses ( context) | |
| Use it in a location |
Long cache for fingerprinted static assets:
location ~* \.(css|js|png|jpg|svg|woff2)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}map
sets one variable from the value of another, and belongs directly inside . It replaces most chains.
map $http_upgrade $connection_upgrade {
default upgrade;
"" close;
}
map $host $backend {
hostnames;
default http://127.0.0.1:3000;
api.example.com http://127.0.0.1:4000;
*.example.com http://127.0.0.1:3000;
}| Syntax | What it does |
|---|---|
| Fallback when nothing matches | |
| Enables style wildcards on the keys | |
| Regex key ( for case-insensitive) | |
| Load key-value pairs from a file |
Variables
Variables are read anywhere a directive takes a value, and any request header is available as with dashes turned into underscores.
| Variable or directive | What it holds |
|---|---|
| Hostname from the request line, or the header | |
| Current URI after rewrites, normalized, no query string | |
| Original URI exactly as sent, query string included | |
| The query string ( for one parameter) | |
| Client IP as nginx sees it | |
| or | |
| , , and so on | |
| , | Response code and duration, for |
| Any request header; reads response headers | |
| Define your own variable (, ) | |
| Set a variable from the client IP ( context) | |
| DNS resolver; required when a URL contains a variable |
Includes and File Locations
| Path or directive | What it is |
|---|---|
| Main config; everything else is included from here | |
| Auto-included site configs (RHEL, Alpine, official packages) | |
| Site configs on Debian/Ubuntu, inactive until linked | |
| Symlinks to the active site configs | |
| File-extension to Content-Type table | |
| Reuse a shared fragment (path relative to ) | |
| Enable a Debian-style site, then reload | |
| Disable it (the file in sites-available stays) | |
| Find which included file defines what |
Logs and Error Pages
| Directive | What it does |
|---|---|
| One line per request | |
| Silence a noisy location (health checks) | |
| Levels: , , , , , | |
| Custom line format, used via | |
| Serve a custom page for a status code | |
| One page for several codes | |
| The page can be served only by , not by URL | |
| Let handle error statuses that came from the backend | |
| Watch errors live; the first stop for any 500 |
Startup failures land in the systemd journal rather than the error log; see the journalctl cheat sheet for .
Allow, Deny and Auth
Rules are checked top to bottom and the first match wins.
| Directive | What it does |
|---|---|
| Permit a network | |
| Permit one IP | |
| Block everyone else; goes last | |
| Ask for a username and password | |
| Where the credentials live | |
| Let a request pass on allowed IP or valid password |
location /admin/ {
satisfy any;
allow 192.168.1.0/24;
deny all;
auth_basic "Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
}Generate the password file with the htpasswd generator.
Timeouts and Limits
| Directive | What it does |
|---|---|
| How long an idle client connection stays open | |
| Max upload size; the default 1m is behind most 413 errors | |
| How long to wait for the request body | |
| How long to wait on a stalled client while responding | |
| Time to establish the backend connection | |
| Max backend silence; raise for slow APIs throwing 504s | |
| Time to write the request to the backend | |
| Connections per worker ( block) |
Stream
proxies raw TCP and UDP (databases, SMTP, game servers). It sits beside in , never inside it, which is why pasting one into a file returns "stream directive is not allowed here".
stream {
upstream db {
server 10.0.0.10:5432;
}
server {
listen 5432;
proxy_pass db;
}
}| Directive | What it does |
|---|---|
| TCP port to accept on ( for UDP) | |
| Backend or upstream name; no scheme, no URI path | |
| How long a connection may sit idle | |
| Time allowed to reach the backend | |
| Read the SNI hostname without decrypting, then route on | |
| Send the PROXY protocol header so the backend sees the real client IP | |
| has no access log unless you add with a format |
Global and Events
The top of , outside every block. Simple directives here end in a semicolon; block directives take braces and no semicolon.
| Directive | What it does |
|---|---|
| OS user the worker processes run as ( on RHEL) | |
| One worker per CPU core | |
| Raise the file descriptor limit per worker | |
| Where the master process writes its PID | |
| Stay in the foreground, which is what containers want | |
| Load the dynamic modules installed as packages | |
| Connection limit per worker; is its own block | |
| Everything web related goes in here | |
| TCP and UDP proxying, a sibling of |
Modules
means nginx does not recognize the name at all. Either it is misspelled, or the module that provides it was never built into this binary or loaded.
| Command or directive | What it does |
|---|---|
| Print the build's compile flags and every module | |
| Load a dynamic module; main context, before | |
| Debian and Ubuntu: one small conf file per enabled module | |
| Adds the module and its line | |
| Same for ; the pattern is | |
| RHEL and Fedora equivalent () | |
| , , | Third-party; they need a build that ships them (OpenResty for lua) |
Context Rules
Every directive is valid only in certain blocks; using the right directive in the wrong block is the "directive is not allowed here" error. Files under and are included inside , so they can hold , , and blocks.
| Directive | Allowed contexts |
|---|---|
| only | |
| , or nested in another | |
| , | only, never inside |
| , | only |
| only | |
| only | |
| , , | , , (set once, inherited down) |
| , | , |
| , only, never | |
| only | |
| , , | |
| only | |
| only | |
| , , | Main context only, siblings of each other |
| , , , | Main context, top of |
Order and Inheritance
| Rule | Detail |
|---|---|
| Location matching | first, then the longest prefix, then regexes in file order; on the winning prefix skips the regex step |
| matching | Exact name, then , then , then regex in file order, then |
| Inheritance | , , and friends pass down from to to until a child sets them again |
| No merging | One or in a child block discards every inherited one, not just the matching name |
| and | Read top to bottom, first match wins |
| Every rule in the block runs in order; restarts location matching, stops the block | |
| Duplicates | Most directives may appear once per block, or nginx returns "directive is duplicate"; put all hostnames on one line. , , , , and may repeat |
Gotchas
- The trailing slash on changes the URI: with a URI part (), the matched location prefix is stripped; without one (), the path passes through untouched.
- in a location throws away every inherited from and . Repeat the parents' headers or set them all at one level.
- Prefer and over . Inside a location, combined with anything beyond or behaves in famously surprising ways.
- If no block has , the first server block for that port silently answers every unmatched hostname, including raw IP requests.
- When seems to do nothing, check which server block actually answered (), whether the sits in a location the request never matched, and whether the worker user can traverse every parent directory. The error log prints the exact path nginx tried to open.
- ignores statuses that came from a backend until you add , and the page it points at must be servable by the same server block.