That closes out how HTTP carries and labels its payloads. Now we move into running HTTP in production, and the first question is a money question: a small website does not need a whole server to itself, so how does one machine host hundreds of different sites at once? We have already met both halves of the answer earlier in the course, the Host header and SNI. This chapter puts them together.
Here is the puzzle. You have one server with one public IP address. Pointed at that single address are shop.example.com, blog.example.com, and a hundred other domains, each a different website with different content. A request arrives. The server can see the IP it came in on, but that IP is the same for every site it hosts. So how does it know which site you actually wanted?
Virtual hosting is running more than one website on a single server. The word "virtual" is the giveaway: from the outside each site looks like it has its own server, but they all share one machine, and often one IP address.
There were historically two ways to do this, and the difference is worth a sentence because it explains why the modern web looks the way it does.
The old way was IP-based virtual hosting: give the server several IP addresses, one per site, and tell the sites apart by which IP the request landed on. It works, but IPv4 addresses are scarce and expensive, so handing one to every small blog does not scale.
The way the whole web runs today is name-based virtual hosting: one IP, many sites, told apart not by the address but by the name the client asked for. That name travels in the request, and the server reads it to decide which site should answer.

That picture is the entire idea. Three different requests reach one server at one IP. Each one carries a different site name. The server reads the name, finds the matching site, and serves it. Same machine, same address, three completely different websites.
So where does the name live? In plain HTTP, it lives in a request header you have already seen: Host.
Think back to the URL. When you type https://blog.example.com/posts, the browser splits that apart. The hostname blog.example.com is used to find an IP and open a connection, and the path /posts becomes the request target. But the connection is just to an IP, the hostname could be lost at that point. So the browser also writes the hostname into a header, and it rides along inside the request itself:
httpGET /posts HTTP/1.1Host: blog.example.com
That Host: blog.example.com line is the answer to the server's question. The request came in on a shared IP, but the Host header names the exact site the client wanted. The server matches that name against its list of sites, picks blog.example.com, and serves its content instead of shop.example.com's.
This is why Host is the one header HTTP/1.1 makes mandatory. Every other header is optional, but without Host a shared server literally cannot tell which site you mean. A request with no Host to a virtual-hosting server is ambiguous by definition.
A small clarification. The
Hostheader is not the same thing as the IP address, and it does not have to match it. The IP gets your bytes to the right machine; theHostheader tells that machine which of its sites you want. You will see this split clearly in the "Try it now" section, where we send the same machine three differentHostvalues and get three different answers.
There is a catch, and it is the one we flagged back in the TLS chapter. Over HTTPS the Host header is encrypted. It sits inside the HTTP request, and the HTTP request only goes out after the secure connection is already built. But to build that secure connection, the server has to present a certificate first, and on a shared server each site has its own certificate. So the server hits a chicken-and-egg problem: it needs to know which site you want in order to pick a certificate, but the header that tells it lives inside the encryption it has not set up yet.
The fix is SNI, Server Name Indication. Right at the start of the TLS handshake, before any encryption, the browser names the hostname it is trying to reach, in the clear. That early hint lets the server choose the correct certificate for that exact site and finish the handshake. Then, once the connection is encrypted, the Host header arrives inside it and the server uses it to route the request, just like plain HTTP.

So an HTTPS request to a shared server names its destination twice, and the order matters:
Host header, encrypted, inside the request, so the server can pick the right site to serve.They usually carry the same hostname and do the same job at two different moments. SNI is the early, unencrypted version asked at handshake time; Host is the later, encrypted version asked once the tunnel is open. Both exist because the handshake happens before the server can read anything encrypted.
Concretely, how does a server keep its sites apart? On Nginx, each site is a server block, and the routing key is server_name. When a request arrives, Nginx compares the Host header against the server_name of each block and serves the one that matches.

Two blocks, both listening on the same port on the same machine. The only thing that separates them is server_name. A request whose Host is shop.example.com matches the first block and gets served from /var/www/shop; a request whose Host is blog.example.com matches the second and gets served from /var/www/blog. One Nginx process, one IP, two sites, and the Host header is the switch between them.
For HTTPS each block also points at that site's own certificate, which is the certificate SNI told the server to present during the handshake. The server_name that matched the Host header and the certificate SNI selected are for the same site, which is why the two mechanisms line up.
What happens when a request arrives with a Host that matches no server_name, or with no Host at all? The server cannot just guess. It needs a rule.
That rule is the default server, sometimes called the fallback or catch-all. It is one site marked as the one to use when nothing else matches. On Nginx you mark a block as default_server and it absorbs every unmatched request. Without an explicit default, Nginx falls back to the first block defined, so there is always some answer, just not necessarily the one a misdirected request hoped for.
What that fallback actually returns is a deployment choice. Common options:
404 Not Found or 421 Misdirected Request, the honest "I don't host that here."This matters more than it sounds. When you point a brand-new domain at a shared server but have not configured its server_name yet, you do not get an error, you get whatever the default server happens to be, which is sometimes a different customer's site. The mismatch between "the IP answered" and "the wrong site answered" is a classic virtual-hosting confusion, and the fix is always the same: add a server block whose server_name matches the domain.
You do not need to run your own server to watch this happen. GitHub Pages hosts a huge number of sites behind a small set of shared IP addresses, and which site answers depends entirely on the Host you send. We will aim every request at the same IP, 185.199.108.153, and change only the hostname.
curl --resolve lets us do exactly that. It says "for this hostname on this port, skip DNS and use this IP," so the connection always lands on the same machine while the hostname (and therefore the Host header and SNI) changes:
bashcurl -s --resolve pages.github.com:443:185.199.108.153 \https://pages.github.com/ -o /dev/null \-w 'HTTP %{http_code} served by %{remote_ip}\n'
textHTTP 200 served by 185.199.108.153
Now point at the same IP but ask for a different site:
bashcurl -s --resolve cli.github.com:443:185.199.108.153 \https://cli.github.com/ -o /dev/null \-w 'HTTP %{http_code} served by %{remote_ip}\n'
textHTTP 200 served by 185.199.108.153
Both requests hit 185.199.108.153, yet they are clearly different sites. Pull the page titles to prove it:
bashcurl -s --resolve pages.github.com:443:185.199.108.153 https://pages.github.com/ | grep -o '<title>[^<]*</title>'curl -s --resolve cli.github.com:443:185.199.108.153 https://cli.github.com/ | grep -o '<title>[^<]*</title>'
text<title>GitHub Pages | Websites for you and your projects...</title><title>GitHub CLI | Take GitHub to the command line</title>
Same server, same IP, two different websites, separated only by the name you asked for. Finally, ask the same machine for a site it does not host and watch the fallback answer:
bashcurl -s --resolve no-such-site.github.io:443:185.199.108.153 \https://no-such-site.github.io/ -o /dev/null \-w 'HTTP %{http_code} served by %{remote_ip}\n'
textHTTP 404 served by 185.199.108.153

The IP answered every time, but the unknown name got a 404 from GitHub's catch-all rather than a real site. That is name-based virtual hosting, SNI, the Host header, and the default server all visible in four commands.
Sometimes the server's answer is not "here is the content" but "the content lives elsewhere." Next we look at redirects, the status codes and the Location header that tell a client to go ask somewhere else.