backend / protocols / nginx / 03_directives_and_contexts.md

Nginx Directives and Contexts

6 interview angles 6 min read source

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_header directives 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

  • 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

  • “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.