2.6 Includes, Organization, and nginx -T
The mechanics are done; now the engineering. Configs are programs, and programs need architecture, imports, and a debugger. NGINX ships all three.
Basic
What you will learn
includein full: paths, globs, ordering, and what exactly gets spliced.The two proven file layouts — nginx.org style and Debian style — and how to choose between them.
Patterns that scale: snippets, per-service files, environment overlays.
nginx -Tas auditor: verifying includes, hunting strays, diffing before deploys.The deploy workflow that makes config changes safe by construction.
1.
include in full
include mime.types; # relative to the PREFIX (or -p), not the file!
include conf.d/*.conf; # glob: alphabetical order, zero matches OK
include /etc/nginx/ssl-main.conf; # absolute pathThe precise semantics — each one has bitten someone you know:
The included text is spliced verbatim at exactly the include's position and context. A file included at
httplevel containingserver{…}blocks is a normal set of vhosts; the same file included inside alocationwould be a parse error.includehas no context of its own — position is everything.Relative paths resolve against the prefix (
-p/--prefix), not against the including file's directory. This is the #1 include surprise. Ifnginx.confandsnippets/gzip.confsit side by side inconf/, then fromnginx.confthe path isinclude conf/snippets/gzip.conf;— wait, no: relative to the prefix, which containsconf/, so it is exactly that. In the classic prefix layout, an instance'snginx.conflives at<prefix>/conf/nginx.conf, so its includes are written relative to<prefix>/:include mime.types;andinclude conf.d/*.conf;.Globs expand in shell order (alphabetical).
10-maps.confsorts before20-servers.conf— numeric prefixes are the standard trick to make order visible and stable. This matters because maps must parse before servers that use their variables (Lesson 2.4's parse-time rule), and because the first server on a socket with nodefault_servermarker becomes the default (Lesson 2.2's trap).A glob that matches nothing is legal —
include conf.d/*.conf;with an emptyconf.d/just splices nothing. (Useful for drop-in dirs; also a way to silently lose a vhost to a typo'd filename extension.-Tfinds it.)
The example config exercises all of this for real:
nginx.conf includes
conf.d/*.conf, and
conf.d/extra-vhost.conf contributes the gamma vhost — a full server block, spliced at http level, before the servers declared in the main file:
$ curl -s -H 'Host: gamma.example.test' http://127.0.0.1:8201/from
served by the conf.d include, port 82012. The two layouts you will inherit
nginx.org layout (what the official packages use)
etc/nginx/
├── nginx.conf # main + events + http skeleton, includes conf.d
├── mime.types
├── fastcgi_params uwsgi_params scgi_params
├── conf.d/ # drop-in: vhosts, maps, zones (glob-included)
│ └── default.conf # ships with a default server
└── modules/ # load_module .conf stubs (Module 15)Debian layout (what
apt install nginx gives you)
etc/nginx/
├── nginx.conf # includes BOTH…
├── sites-available/ # the library of vhost files (not loaded)
│ └── mysite.conf
├── sites-enabled/ # symlinks to available — THIS is what loads
│ └── mysite.conf -> ../sites-available/mysite.conf
├── conf.d/ # same as above
└── snippets/ # (Ubuntu) small reusable fragmentsNeither is wrong; mixing them silently is. The Debian flow's
enable a site by creating a symlink is genuinely good vhost lifecycle management (
a2ensite habits ported from Apache). The nginx.org flow is flatter and matches every piece of upstream documentation. Choose per fleet, be able to detect which one you're standing in (Lesson 1.3's
nginx -V + a directory listing), and never mix files across the two in one instance.
3. Patterns that scale
Pattern 1 — snippets: defeat array-cancellation. Lesson 2.5's fix, formalized:
conf/
├── nginx.conf
└── snippets/
├── security-headers.conf # 3-4 add_header lines
├── proxy-headers.conf # the proxy_set_header set (Lesson 4.2)
└── tls-modern.conf # protocols+ciphers (Module 6)Any context that uses the directive family re-includes the snippet verbatim. The duplication is the point: it survives inheritance law by construction.
Pattern 2 — one service, one file.
conf.d/
├── 00-maps.conf # all map/geo/zones — must parse first
├── 10-api-gateway.conf # server + its upstreams
├── 20-www-site.conf
├── 30-admin-site.conf
└── 90-default-catchall.conf # the default_server, last by conventionNumeric prefixes make parse order a
visible contract: variables/zones first, services middle, catch-all last. A new service is a new file — no merge conflicts, reviewable in isolation, deletable in one
rm.
Pattern 3 — environment overlays. Same core, different per environment:
nginx.conf → include conf.d/*.conf
include env/production/*.conf # via a build flag or symlinkor per-host:
# nginx.conf
include conf.d/*.conf;
include env.d/${HOSTROLE}/*.conf; # ← not a variable: include takes NO
# runtime variables. Do this with a
# symlink at deploy time instead:
# env.d -> env.d-prod4.
nginx -T — the auditor
-T =
-t +
dump the fully expanded, effective configuration — every include resolved, printed with
# configuration file <path>: banners:
$ nginx -T 2>/dev/null | head -5
# configuration file /etc/nginx/nginx.conf:
worker_processes auto;
…
$ nginx -T 2>/dev/null | grep '# configuration'
# configuration file /etc/nginx/nginx.conf:
# configuration file /etc/nginx/conf.d/extra-vhost.conf:The three professional uses:
Audit 1 — "what is actually loaded?" The file banners are the authoritative list. A vhost not answering? If its file isn't in the banner list, it was never included — wrong directory, wrong suffix, wrong instance. (This catches the
.conf.bak and
mysite.conf~ strays that globs silently ignore.)
Audit 2 — "what is the effective value?" Inheritance (Lesson 2.5) resolved: search the dump for the directive near your location of interest. What
-T prints is what the master parsed — no guessing about which file won.
Audit 3 — "what changed?" Diff before/after deploys:
nginx -T 2>/dev/null > /tmp/before.conf
# …apply config change…
nginx -T 2>/dev/null > /tmp/after.conf
diff -u /tmp/before.conf /tmp/after.conf # the ENTIRE change, nothing hiddenCI pipelines wrap this: fail the deploy if the diff contains anything outside the files the PR touched — catching "helpful" colleagues and stale-checkout drift simultaneously.
Run all three of these greps against the
nginx -T dump — in our example: includes listed, gamma's server present, maps visible.
5. The safe-deploy workflow, assembled
Everything from Modules 1–2 converges into the six-line ritual that every serious NGINX fleet runs:
# 1. render/template configs into place (env overlays, secrets)
# 2. validate WITHOUT touching the running master:
nginx -t # syntax + files + temp dirs
nginx -T | grep -E 'listen|server_name' # eyeball listeners/vhosts
# 3. ship it live:
nginx -s reload # atomic from the master's side
# 4. confirm the new generation actually took over:
tail -f …/error.log # "reconfiguring"… then no [emerg]
ps aux | grep [n]ginx # old workers draining → gone
# 5. (regression) hit your canary endpoints:
curl -fsS http://127.0.0.1:8200/healthz
# 6. keep the -T dump as an artifact for postmortems
nginx -T > /var/log/nginx-config-$(date +%F).conf
nginx -t && nginx -s reload is exactly steps 2–4 in one line — and by the time you finish this series you will have run this ritual so many times it's muscle memory.
Try it yourself
Watch order matter. Rename
conf.d/extra-vhost.conftoconf.d/00-early.conf, reload, and dump:
nginx -T 2>/dev/null | grep '# configuration'The banner order flips — gamma's server now parses
before the maps. (It still works: gamma uses no mapped variables. Add one
return 200 "$env_label"; to gamma and
now the reorder breaks it —
unknown "env_label" variable — Lesson 2.4's parse rule, live.)
Build your own snippet. Extract alpha's
add_header X-Server alpha always;intosnippets/server-tag.conf, include it in alpha and beta, reload, verify both still tag correctly.Stray-file hunt. Copy
extra-vhost.conftoextra-vhost.conf.bakandextra-vhost.txt. Reload; find which one loaded via-Tbanners (only.confmatches the glob). Delete the strays.Diff a change. Capture
-T, tweak any timeout, capture again, anddiff— one line, exactly the change you made.Final sweep: re-run nginx -T and confirm the include banners are back to exactly what you expect.
Q&A
Q1. Why does
include $ENV/ssl.conf; fail?
Answer
include takes no runtime variables — it's resolved at config
parse time, before any request (or variable) exists. Dynamic config selection must happen before NGINX starts: templates, symlinks, or deploy-time rendering (Module 13 automates the pattern).
Q2. Your glob-included vhost directory has
app.conf,
app.conf~, and
app.conf.disabled. Which load?
Answer
Only
app.conf matches
*.conf… careful:
app.conf~ and
app.conf.disabled do NOT match
*.conf (suffix mismatch) — but
app.conf.disabled doesn't, while a file named exactly
app.disabled.conf WOULD. The trap is editor backups that end in
.conf~ on some tools.
nginx -T banners settle it definitively.
Q3. After a deploy, one vhost serves old content.
nginx -t passes, the error log shows a clean reload. Give the two
-T-based checks you run next.
Answer
(1) Is the vhost's file in the
-T banner list at all (include path / suffix / instance)? (2) Is the effective directive in the dump what you expect — or is another file's server winning (duplicate
server_name, differing
listen port) per Lesson 2.2's selection rules?
Q4. Why do the numeric prefixes (
00-,
10-,
90-) exist if
server selection is by name-matching rules, not file order?
Answer
File order still governs: regex
server_name precedence, "first server on a socket becomes default when unmarked", and map-before-use parsing. Prefixes make all three deterministic and
visible — the contract survives even when a teammate adds files.
Takeaways
includesplices text at its exact position and context; paths are prefix-relative; globs expand alphabetically and may legally match nothing.Two sane layouts (nginx.org flat-drop-in vs Debian sites-enabled); detect, choose, and never mix.
Scale with snippets (defeating array-cancellation), one-service-one-file with numeric prefixes, and deploy-time environment overlays — because include is compile-time, by design.
nginx -Tis the auditor: authoritative include list, resolved inheritance, and diffable deploy artifacts. Three audits, one command.Modules 1–2 have now given you the complete static picture of NGINX: architecture, control, grammar, and engineering. Everything ahead is application of this foundation.
Module 2 complete. Next: Module 3 — putting the machinery to work:
root vs
alias (the eternal confusion, settled),
try_files and the SPA pattern, MIME types, sendfile, and the complete theory of serving files fast.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.