Backend / Protocols / nginx / 03_directives_and_contexts.md

Nginx Directives and Contexts

Updated 6 interview angles 6 min read source
On this page13
  1. The context tree
  2. Where directives are allowed
  3. Inheritance
  4. The add_header trap
  5. include for organization
  6. Variables — built-in
  7. Custom variables — set and map
  8. The if directive — handle with care
  9. return vs rewrite
  10. Reloading after changes
  11. A useful directive cheat sheet
  12. Common interview confusions
  13. Interview angle

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

text
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:

text
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:

nginx
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_header directives defined on the current level.

nginx
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:

nginx
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:

nginx
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

nginx
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:

nginx
# 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:

nginx
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

bash
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

  • if works the same as in any language.” — it has known broken interactions (especially proxy_pass inside if). Prefer map and separate location blocks.
  • add_header always adds headers.” — only if the current context has none. Headers from outer context are silently dropped if you add_header anything 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 6

  • “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_header and proxy_set_header reset entirely if redefined deeper.
  • “Why is if discouraged?” — works for some directives but breaks subtly with proxy_pass/fastcgi_pass inside location. Use map for branching and separate location blocks.
  • “What’s the difference between return and rewrite?”return ends the request (use for redirects and fixed responses); rewrite modifies the URI and may continue processing.
  • “How do you safely reload nginx after a config change?”nginx -t first to validate; if it passes, nginx -s reload. Reload is graceful: in-flight requests finish on old workers.
  • “What’s the difference between the http and stream blocks?”http is HTTP-aware (L7), stream is raw TCP/UDP proxy (L4). Use stream for proxying Postgres/Redis/raw TCP.