Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Beginner ~40 min read

Basic

What you will learn

  1. The inheritance model in one sentence, then in full precision.

  2. The three directive personalities: standard (replace), array (append at one level, cancel from the previous), and special cases that don't inherit at all.

  3. The add_header surprise — captured live, with the two-line fix.

  4. root inheritance proven through the error log.

  5. 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:

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

nginx
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/site

Simple, 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:

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

bash
$ 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)
bash
$ 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
bash
$ 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:

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

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

nginx
server {
    root www/main;          # the only root in sight
    location /inherit/ { }  # silent — borrows it
}
bash
$ 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:

text
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; a location can turn on debug for one endpoint (Module 16's microscope, error_log … debug per-location).

  • access_log: an array! Multiple logs at one level all fire; a deeper level that sets any access_log cancels inherited ones — same law as add_header, with off as the sanctioned silence. The /quiet/ and /loud/ locations demonstrate all of it:

bash
$ 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 it

Try it yourself

  1. Reproduce all three header experiments of §3 with curl -sD -. Count X-Server lines with grep -c '^X-Server:'.

  2. Prove root inheritance your way. Create /var/www/main/inherit/present.txt, request it, then request a missing file and read the resolved path from error.log.

  3. The proxy_set_header preview. 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.

  4. Break access_log 's array law. Give /loud/ a second access_log line (two files) — both receive the request; then move one up to the server and watch the location cancel it.

  5. 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_header cancellation is the single most common NGINX bug in the wild — fix with re-declaration via include snippets, enforce with nginx -T audits.

  • A 404's error.log line prints the fully resolved filesystem path — free, forensic proof of what root inheritance actually did.

  • When in doubt, run the three-question decision procedure; it's total and takes ten seconds.

Was this page helpful?

Mark this learning resource complete to track your learning progress.