Nginx Directives and Contexts
Nginx config is a tree of nested blocks (called “contexts”). Directives are valid in specific contexts. Knowing the hierarchy is the first step to reading any unfamiliar config.
The context tree
main ← top of nginx.conf
├── worker_processes auto;
├── user www-data;
│
├── events { ← worker connection settings
│ worker_connections 1024;
│ }
│
└── http { ← all HTTP-related config
sendfile on;
gzip on;
log_format main '...';
upstream backend {
server 10.0.0.1:8000;
}
server { ← one virtual host
listen 443 ssl;
server_name api.example.com;
ssl_certificate ...;
location / { ← one URL match block
proxy_pass http://backend;
}
location /static/ {
alias /var/www/static/;
}
}
}
For TCP/UDP proxying (not HTTP) there’s also:
stream { ← peer of `http`, for TCP/UDP
upstream pg_pool { server 10.0.0.5:5432; }
server {
listen 5432;
proxy_pass pg_pool;
}
}
Where directives are allowed
Each directive lives in a specific context. The nginx docs list this for every directive (the “Context:” line). Examples:
| Directive | Allowed in |
|---|---|
worker_processes |
main only |
events (block) |
main only |
http (block) |
main only |
server (block) |
http, mail, stream |
location (block) |
server, location |
proxy_pass |
location, limit_except |
gzip |
http, server, location |
Putting a directive in the wrong context = nginx: [emerg] "X" directive is not allowed here on nginx -t.
Inheritance
Directives in outer contexts apply to inner ones unless overridden:
http {
gzip on; # default for all servers/locations
server {
gzip off; # this server: no gzip
location /api/ {
gzip on; # except /api/, gzip back on
}
}
}
Mostly intuitive, with surprises around things like index, proxy_set_header (resets when you set any in a deeper level — see below), and add_header.
The add_header trap
add_header does NOT inherit if the inner block has its own add_header. From nginx docs:
These directives are inherited from the previous configuration level if and only if there are no
add_headerdirectives defined on the current level.
server {
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
location /api/ {
add_header X-API-Version "2"; # X-Frame-Options and X-Content-Type-Options NO LONGER set here
}
}
Fix: re-add inherited headers in the inner block, or use the more_set_headers directive from nginx-more-headers module (which doesn’t have this quirk).
Same trap with proxy_set_header. Set them all in one place (a snippet you include).
include for organization
Big configs split into files:
http {
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*; # Debian/Ubuntu convention
}
include is a literal text inclusion at parse time. Useful for:
- Per-site server blocks in
sites-enabled/. - Reusable snippets (
include /etc/nginx/snippets/security-headers.conf;). - mime types.
Variables — built-in
Nginx exposes request data as $variables:
| Variable | Value |
|---|---|
$host |
request Host header (lowercased) |
$request_uri |
full URI with args |
$uri |
normalized URI without args |
$args / $query_string |
query string |
$remote_addr |
client IP (before proxy headers) |
$proxy_add_x_forwarded_for |
adds $remote_addr to existing X-Forwarded-For |
$scheme |
http or https |
$request_method |
GET, POST, etc. |
$http_<header> |
any request header ($http_user_agent) |
$cookie_<name> |
a cookie value |
$server_name |
matched server_name |
$status |
response status (in log_format) |
$body_bytes_sent |
bytes in response body |
$request_time |
total request time in seconds |
$upstream_response_time |
upstream’s part of that time |
Used in directives:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
log_format main '$remote_addr $request $status $request_time';
Custom variables — set and map
set $my_var "hello"; # always — unconditional
# map: lookup table on another variable's value
map $http_user_agent $is_bot {
default 0;
~*googlebot 1;
~*bingbot 1;
}
server {
if ($is_bot) {
access_log off;
}
}
map is far cheaper than if chains and is the right tool for “compute X based on Y.”
The if directive — handle with care
The famous nginx warning: “if is evil” in location context. It works for some things (mostly variable comparisons) but breaks subtly with others (like proxy_pass inside if). The official guidance:
# Safe in `if`:
return 301 ...;
rewrite ...;
set $x ...;
# Unsafe in `if` inside location:
proxy_pass ...;
fastcgi_pass ...;
Prefer map for branching, separate location blocks, or try_files for fallback chains.
return vs rewrite
Both modify the response or URI:
return 301 https://$host$request_uri; # immediate response, no further processing
rewrite ^/old/(.*) /new/$1 permanent; # rewrites URI, may keep processing
Prefer return when the response is final (redirects, fixed responses). rewrite is for actual URL transformation that other directives still process.
Reloading after changes
nginx -t # validate config; never skip this
nginx -s reload # graceful reload if -t passes
-t catches typos, missing files, undefined upstreams. Always run before reload — a broken config will refuse to load and you’ll lose your in-flight upgrade.
A useful directive cheat sheet
| What you want | Directive |
|---|---|
| Listen on port | listen 443 ssl http2; |
| Match a domain | server_name api.example.com; |
| Forward to backend | proxy_pass http://backend; |
| Serve static files | root /var/www; or alias /var/www/static/; |
| Allow upload size | client_max_body_size 50M; |
| Set timeouts | proxy_read_timeout 60s; |
| Add response header | add_header X-Custom value always; |
| Pass header to upstream | proxy_set_header Host $host; |
| Enable gzip | gzip on; gzip_types text/plain ...; |
| Rate limit | limit_req zone=api burst=20; |
| Redirect | return 301 https://$host$request_uri; |
| Block IPs | deny 192.0.2.0/24; allow all; |
Common interview confusions
- “
ifworks the same as in any language.” — it has known broken interactions (especiallyproxy_passinsideif). Prefermapand separatelocationblocks. - “
add_headeralways adds headers.” — only if the current context has none. Headers from outer context are silently dropped if youadd_headeranything in the inner. - “Reload re-reads everything immediately.” — yes, but old workers keep handling in-flight requests with the old config until they drain.
- “Config syntax is YAML/JSON.” — it’s a custom syntax: nested
{},;to terminate directives,#for comments.
Interview angle
- “Walk me through nginx’s config structure.” — main → events / http (also stream/mail) → server → location. Each context has specific directives allowed.
- “How does directive inheritance work?” — outer context directives apply to inner unless overridden. Notable exceptions:
add_headerandproxy_set_headerreset entirely if redefined deeper. - “Why is
ifdiscouraged?” — works for some directives but breaks subtly withproxy_pass/fastcgi_passinsidelocation. Usemapfor branching and separatelocationblocks. - “What’s the difference between
returnandrewrite?” —returnends the request (use for redirects and fixed responses);rewritemodifies the URI and may continue processing. - “How do you safely reload nginx after a config change?” —
nginx -tfirst to validate; if it passes,nginx -s reload. Reload is graceful: in-flight requests finish on old workers. - “What’s the difference between the
httpandstreamblocks?” —httpis HTTP-aware (L7),streamis raw TCP/UDP proxy (L4). Usestreamfor proxying Postgres/Redis/raw TCP.