2.2 Server Blocks & Virtual Hosts
One IP, one port, many sites: the exact algorithm NGINX uses to pick a server{}, with every claim verified against a running server.
Basic
What you will learn
What a
server{}block really is: an identity (listen+server_name) plus a policy.Name-based virtual hosting — how the
Hostheader drives selection.The complete server-selection algorithm, in order, with live evidence for each step.
default_server— your safety net for unknown, absent, and hostileHostheaders.Running many ports, and mixing
listenvariants.
1. What a server block is
server {
listen 8200; # WHERE: address/port this server answers
server_name alpha.example.test; # WHO: which Host values claim it
… # WHAT: root, locations, headers, logs…
}A
server{} is the answer to three questions:
Where — the sockets it may be selected on (
listen). Multiple servers may share a socket; that sharing is the whole point.Who — the
Host:request header values that select it (server_name).What — everything else in the block: the policy applied to requests that land here.
The example config runs
five servers on one socket (
127.0.0.1:8200) plus a sixth on
:8201 (via
conf.d/). Here is the selection matrix, captured live — burn this table into memory:
$ for h in alpha.example.test beta.example.test random.example.test \
app-acme.apps.test unknown.host.xyz; do
printf '%-22s -> %s\n' "$h" "$(curl -s -H "Host: $h" http://127.0.0.1:8200/ | head -1)"
done
alpha.example.test -> <!DOCTYPE html> (alpha: serves www/main)
beta.example.test -> <!DOCTYPE html> (beta: serves www/spa)
random.example.test -> matched: wildcard .example.test (host=random.example.test)
app-acme.apps.test -> matched: regex vhost, tenant=acme
unknown.host.xyz -> matched: default_server catch-all (host=unknown.host.xyz)Same IP. Same port. Five different answers. That is name-based virtual hosting, and the
Host header is the entire steering signal.
2. The server-selection algorithm
When a request arrives on a listening socket, NGINX runs this exact sequence:
1. Filter by socket: only servers whose listen matches the connection
2. Read the Host header: strip the port (":8200"), lowercase it
3. Try EXACT names: server_name alpha.example.test — first hit wins
4. Try LEADING wildcards: *.example.test — LONGEST match wins
(".example.test" is shorthand for example.test + *.example.test)
5. Try TRAILING wildcards: www.example.* — LONGEST match wins
6. Try REGEX names: ~^app-(?<tenant>…)$ — FIRST in config order wins
7. Fall back: the default_server for that socket
(the one marked, or the FIRST server on that port)Numbers 3–7 are strictly ordered: an exact match anywhere beats every wildcard; any wildcard beats every regex; regexes only see hosts the wildcards declined. Two of these orderings are already visible in the capture above:
alpha.example.test→ the exact server, even though the wildcard.example.testalso matches it. Exactness wins.random.example.test→ the wildcard, even though a regex could have been written to match. Wildcards are consulted first, period.
The capture trick when you do use regex names
server {
listen 8200;
server_name ~^app-(?<tenant>[a-z0-9]+)\.apps\.test$;
return 200 "matched: regex vhost, tenant=$tenant\n";
}Named captures (
?<tenant>) become
variables usable anywhere in the server —
$tenant here. That single feature is why regex names survive.
$ curl -s -H "Host: app-acme.apps.test" http://127.0.0.1:8200/
matched: regex vhost, tenant=acme3.
default_server — the safety net
server {
listen 8200 default_server;
server_name _; # "_" is an invalid domain: pure convention
…
}Whatever request arrives on
:8200 that no other server claims — unknown name, a raw IP as Host, a scanner sending garbage — lands here. Three professional uses:
A polite landing page (a common choice): explain the site, don't leak your real vhost's existence.
A black hole:
return 444;— NGINX's proprietary "close connection without response" code. Scanners get silence. (Module 7 builds a full IP-reputation version of this.)A redirect farm: catch
example.test+ typos and 301 them towww.example.test.
Also note:
default_server is
per socket (
listen group), not global — a server listening on
8200 and another on
8201 each want their own.
4. More sockets, more ports
The identity rule worth stating out loud:
a server is selectable only on the sockets it listens on. The gamma vhost proves it — it lives in
conf.d/extra-vhost.conf with
listen 8201 and simply cannot be reached through 8200:
$ curl -s -H "Host: gamma.example.test" http://127.0.0.1:8201/from
served by the conf.d include, port 8201
listen accepts richer forms that you'll use in production:
listen 127.0.0.1:8080; # specific address (localhost only)
listen 443 ssl; # protocol flags (Module 6)
listen [::]:80; # IPv6 (all v6 addresses)
listen 8080 reuseport; # per-worker accept queues (Module 9)
listen 443 quic reuseport; # HTTP/3 (Module 9)
listen 80 default_server; # claim the fallback slot
listen 8080 backlog=2048; # kernel accept-queue depth (Module 9)And
server_name accepts lists and both wildcard shapes:
server_name example.test www.example.test example.test.; # exact list
server_name *.example.test; # leading wildcard (one level)
server_name mail.*; # trailing wildcard
server_name .example.test; # domain + ONE-level wildcard, combined
server_name ~^app-(?<t>[a-z0-9]+)\.apps\.test$; # regex (last resort)5. How this maps onto the request walk
Putting Lessons 2.1–2.2 together, a request's first steps through the config tree are now fully specified:
connection accepted on a listening socket
└─ choose ONE server{} ← this lesson
└─ choose ONE location{} ← next lesson
└─ handler + inherited directivesEverything "above" the chosen server (http-level directives) still applies through inheritance (Lesson 2.5). Everything below — the locations — is next.
Try it yourself
Against a running instance, every experiment is a one-liner:
Reproduce the selection matrix (§1) yourself, then add your own cases:
Host: ALPHA.EXAMPLE.TEST(case-insensitivity),Host: a.b.example.test(multi-level — wildcard.example.testdeclines it!),Host: app-42.apps.test.Watch the default server catch strays. Send a request with no Host at all (HTTP/1.0):
bashcurl -s --http1.0 http://127.0.0.1:8200/ | head -1Prove exact beats wildcard.
alpha.example.testmatches both alpha (exact) and.example.test(wildcard) — confirm which answered via theX-Serverresponse header:bashcurl -sD - -o /dev/null -H 'Host: alpha.example.test' http://127.0.0.1:8200/ | grep X-ServerInspect, don’t trust. Ask the instance what servers it has:
bashnginx -T | grep server_name
Q&A
Q1. Requests for
deep.sub.example.test hit your default server, but
sub.example.test works. Why?
Answer
.example.test /
*.example.test match exactly
one level of subdomain.
deep.sub.… has two. Either add
*.sub.example.test, a regex name, or a dedicated server — in that order of preference.
Q2. Two servers both declare
listen 8200 default_server;. What happens?
Answer
Startup fails:
[emerg] a duplicate default server for 127.0.0.1:8200. The fallback role is exclusive per socket.
Q3. Why is
server_name _; conventional rather than magical?
Answer
_ is simply an invalid hostname that will never match a real client request through steps 3–6; the server is therefore reachable
only as the default. Any impossible name (
__catch__,
null) works identically.
Q4. Your TLS site needs to answer both
example.test and
*.example.test with one certificate-covered server. Minimal
server_name?
Answer
server_name .example.test; — the dot shorthand covers the bare domain and one level of subdomains. (Certificate SANs must cover the same set — Module 6 ties these together.)
Takeaways
A
server{}= where (listen) + who (server_name) + what (policy).Selection order is fixed: exact → longest leading wildcard → longest trailing wildcard → first regex in config order →
default_server.default_serveris per-socket, must be explicit, and is where unknown, dotted, and hostile Hosts land — design it deliberately.Regex server names are a last resort whose superpower is named captures (
$tenant) — keep at most one pattern-shaped server.One server per port group can be "moved" by reordering includes — mark your defaults and your config becomes order-independent.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.