2.5 Directive Inheritance: The One Rule
"Why did my header disappear?" has exactly one answer, and after this lesson you'll never ask it again.
Basic
What you will learn
The inheritance model in one sentence, then in full precision.
The three directive personalities: standard (replace), array (append at one level, cancel from the previous), and special cases that don't inherit at all.
The
add_headersurprise — captured live, with the two-line fix.rootinheritance proven through the error log.A decision procedure for any directive at any level, in 10 seconds.
1. The model in one sentence
A directive's effective value at a request is whatever the deepest context on the request's path explicitly set — or, if that context set nothing for it, whatever the context above set — climbing to the top.
The request's path is the tree walk from Lesson 2.1:
http{} → server{} → location{} (→ nested location)Each directive independently climbs this ladder. There is no "merge": within a context, a directive either has a value (set right there) or borrows one from above.
http { root /var/www/default; }
server { root /var/www/site; } # server wins for its requests
# …EXCEPT where location speaks:
location /downloads/ { root /data; } # this location uses /data
location /help/ { } # silent -> borrows /var/www/siteSimple, general, and — for six or seven directives — deliberately not true. Those exceptions are where careers get interrupted.
2. The three directive personalities
Personality A — standard: deepest explicit value wins
root,
index,
keepalive_timeout,
client_max_body_size,
proxy_read_timeout,
gzip … the overwhelming majority. Inheritance here is the borrow-or-override you expect:
http {
client_max_body_size 10m; # fleet default
server {
server_name uploads.example.test;
client_max_body_size 100m; # this vhost overrides
location /avatar/ {
client_max_body_size 2m; # and this endpoint tightens again
}
}
}Requests to
/avatar/ get 2 MB; other paths on the vhost, 100 MB; every other vhost, 10 MB. Three ladders, one rule.
Personality B — arrays: same level appends, deeper level cancels
add_header,
listen,
server_name,
include,
access_log (multiple logs),
proxy_set_header,
ssl_certificate pairs… These directives can be repeated
within one context (they accumulate), and that accumulation is what breaks intuition
across levels:
When a context defines any directive of an array type, it does NOT inherit the ones from above. It starts fresh.
Personality C — non-inheriting by design
listen,
server_name (server-only),
try_files (per-location by nature), rewrite-phase directives, and anything whose meaning would be ambiguous after a move. No ladder, no borrowing.
3. The
add_header surprise — in practice
The example server has three sibling locations that demonstrate Personality B perfectly. The server sets one header; each location plays a different move. Here is the raw evidence (curl
-D):
$ curl -sD - -o /dev/null -H 'Host: alpha.example.test' http://127.0.0.1:8200/extra/
HTTP/1.1 200 OK
…
X-Extra: bonus ← location's own header
# (no X-Server! the inherited one was CANCELLED)$ curl -sD - -o /dev/null -H 'Host: alpha.example.test' http://127.0.0.1:8200/both/
HTTP/1.1 200 OK
…
X-Server: alpha ← re-declared in the location
X-Extra: bonus ← plus the local one$ curl -sD - -o /dev/null -H 'Host: alpha.example.test' http://127.0.0.1:8200/fresh/
HTTP/1.1 200 OK
…
X-Server: fresh ← replaced
# (exactly ONE X-Server line — grep -c proves it)The corresponding config, condensed to its teaching skeleton:
server {
add_header X-Server alpha always; # server-level truth…
location /extra/ {
add_header X-Extra bonus always; # …CANCELLED here: X-Server gone
}
location /both/ {
add_header X-Server alpha always; # re-declared: both survive
add_header X-Extra bonus always;
}
location /fresh/ {
add_header X-Server fresh always; # replaced
}
}
The two-line fix, used everywhere in production: if a location needs
its own header plus the server's security headers, the location must repeat the entire set. This is why teams put shared headers in an
included snippet:
# conf.d/security-headers.conf (an include, not inheritance!)
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Strict-Transport-Security "max-age=31536000" always;
# usage — every context that adds a header re-includes the set:
server { include conf.d/security-headers.conf; … }
location /special/ { include conf.d/security-headers.conf;
add_header X-Special 1 always; }4.
root inheritance, proven by the error log
Personality A is easy to
claim. The proof is forensic — the
/inherit/ location is empty on purpose, so the file resolution must use the server-level
root:
server {
root www/main; # the only root in sight
location /inherit/ { } # silent — borrows it
}$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: alpha.example.test' \
http://127.0.0.1:8200/inherit/README
200 # found: www/main/inherit/README
$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: alpha.example.test' \
http://127.0.0.1:8200/inherit/nope
404
$ tail -1 /var/log/nginx/error.log
2026/09/22 … [error] … open() "/var/www/main/inherit/nope" failed \
(2: No such file or directory) …The 404's error line prints the
fully resolved path —
…/www/main/inherit/nope — receipt-grade proof that the location borrowed the server's
root. (This trick —
read the resolved path out of a 404 — is a general debugging tool; Module 3 leans on it heavily for root/alias problems.)
5. The 10-second decision procedure
For any directive at any level, answer three questions:
Q1. Is it legal in this context at all? (Lesson 2.1's table)
no → error at parse; move it.
Q2. Is it an ARRAY directive (repeatable in one context)?
yes → does THIS context have any of them?
yes → the ones here are the complete set. Re-declare
anything you want to keep from above.
no → inherit from above (all of them).
no → does THIS context set it?
yes → its value wins. Full stop.
no → inherit from above.
Q3. Is it one of the non-inheriting mavericks (listen/server_name/
try_files/rewrite-phase)? → no ladder; it acts where it lives.6.
error_log and
access_log: a special pleasure
Both are legal at main/http/server/location — a rare four-level ladder — and they compose rather than replace, which makes them the friendly exception to learn after the rule:
error_log: the deepest configured level wins per request; alocationcan turn ondebugfor one endpoint (Module 16's microscope,error_log … debugper-location).access_log: an array! Multiple logs at one level all fire; a deeper level that sets anyaccess_logcancels inherited ones — same law asadd_header, withoffas the sanctioned silence. The/quiet/and/loud/locations demonstrate all of it:
$ curl … /about.html ; curl … /quiet/ ; curl … /loud/
$ grep -c "GET /about.html" /var/log/nginx/access.log
1
$ grep -c "GET /quiet/" /var/log/nginx/access.log
0 # access_log off
$ grep -c "GET /loud/" /var/log/nginx/loud.log
1 # override redirects, doesn't add
$ grep -c "GET /loud/" /var/log/nginx/access.log
0 # …and the inherited log lost itTry it yourself
Reproduce all three header experiments of §3 with
curl -sD -. CountX-Serverlines withgrep -c '^X-Server:'.Prove root inheritance your way. Create
/var/www/main/inherit/present.txt, request it, then request a missing file and read the resolved path fromerror.log.The
proxy_set_headerpreview. Add to a location:proxy_set_header X-Demo 1;— then read the module's Lesson 4.2 after finishing Module 2 and recognize this exact law in the wild.Break
access_log's array law. Give/loud/a secondaccess_logline (two files) — both receive the request; then move one up to the server and watch the location cancel it.Verify the whole playground survived your surgery — nginx -t passes and the §3 receipts still reproduce.
Q&A
Q1. Server sets
add_header X-A 1 always; add_header X-B 2 always;. A location adds
add_header X-C 3 always;. Which headers does a response from that location carry?
Answer
Only
X-C. Any array-directive use in a context cancels
all inherited ones of that type. To keep A and B, re-declare them in the location (ideally via an include snippet).
Q2. Same setup, but the location's directive is
root /data; instead. Which headers now?
Answer
X-A and
X-B —
root is Personality A and completely unrelated to the header array. Each directive climbs (or cancels) its own ladder, independently.
Q3.
http sets
access_log logs/a.log main;.
server sets nothing.
location /x/ sets
access_log logs/x.log main;. Where is a request to
/x/1 logged? A request to
/y/?
Answer
/x/1 → only
x.log (cancel-and-replace at the location level).
/y/ →
a.log (inherited through the silent server).
Q4. Your security team demands three headers on every response of every vhost. One config line in
http{} achieves it — until a developer adds a per-location
add_header for a feature. What went wrong and what's the professional fix?
Answer
The location's new header cancelled the http-level security set for its responses. Fix: move the security set into an include snippet and include it in
every context that uses
add_header (or enforce via CI:
nginx -T must show the snippet included in each such context — Lesson 2.6 gives you the audit command).
Takeaways
One sentence: deepest explicit setting wins; silent contexts borrow.
Personality A (standard) overrides; Personality B (arrays:
add_header,proxy_set_header,access_log, …) cancels from above and rebuilds within the level; Personality C mavericks don't inherit.The
add_header/proxy_set_headercancellation is the single most common NGINX bug in the wild — fix with re-declaration via include snippets, enforce withnginx -Taudits.A 404's
error.logline prints the fully resolved filesystem path — free, forensic proof of whatrootinheritance actually did.When in doubt, run the three-question decision procedure; it's total and takes ten seconds.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.