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

Levels: Beginner ~35 min read

Basic

What you will learn

  1. include in full: paths, globs, ordering, and what exactly gets spliced.

  2. The two proven file layouts — nginx.org style and Debian style — and how to choose between them.

  3. Patterns that scale: snippets, per-service files, environment overlays.

  4. nginx -T as auditor: verifying includes, hunting strays, diffing before deploys.

  5. The deploy workflow that makes config changes safe by construction.


1. include in full

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

The 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 http level containing server{…} blocks is a normal set of vhosts; the same file included inside a location would be a parse error. include has 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. If nginx.conf and snippets/gzip.conf sit side by side in conf/, then from nginx.conf the path is include conf/snippets/gzip.conf; — wait, no: relative to the prefix, which contains conf/, so it is exactly that. In the classic prefix layout, an instance's nginx.conf lives at <prefix>/conf/nginx.conf, so its includes are written relative to <prefix>/: include mime.types; and include conf.d/*.conf;.

  • Globs expand in shell order (alphabetical). 10-maps.conf sorts before 20-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 no default_server marker becomes the default (Lesson 2.2's trap).

  • A glob that matches nothing is legal — include conf.d/*.conf; with an empty conf.d/ just splices nothing. (Useful for drop-in dirs; also a way to silently lose a vhost to a typo'd filename extension. -T finds 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:

bash
$ curl -s -H 'Host: gamma.example.test' http://127.0.0.1:8201/from
served by the conf.d include, port 8201

2. The two layouts you will inherit

nginx.org layout (what the official packages use)

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

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

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

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

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

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

text
nginx.conf → include conf.d/*.conf
             include env/production/*.conf     # via a build flag or symlink

or per-host:

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

4. nginx -T — the auditor

-T = -t + dump the fully expanded, effective configuration — every include resolved, printed with # configuration file <path>: banners:

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

bash
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 hidden

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

bash
# 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

  1. Watch order matter. Rename conf.d/extra-vhost.conf to conf.d/00-early.conf, reload, and dump:

bash
   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.)

  1. Build your own snippet. Extract alpha's add_header X-Server alpha always; into snippets/server-tag.conf, include it in alpha and beta, reload, verify both still tag correctly.

  2. Stray-file hunt. Copy extra-vhost.conf to extra-vhost.conf.bak and extra-vhost.txt. Reload; find which one loaded via -T banners (only .conf matches the glob). Delete the strays.

  3. Diff a change. Capture -T, tweak any timeout, capture again, and diff — one line, exactly the change you made.

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

  • include splices 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 -T is 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.

Was this page helpful?

Mark this learning resource complete to track your learning progress.