Problem RestatementProblem
Google asked a practical networking question: you have one Linux server with one public IPv4 address. A coffee shop, a butcher shop and an auto repair shop each want their own website on their own domain (coffee.example, butcher.example, auto.example). How do you host all three on this one machine? How does the right site show up for each domain, and how do you support HTTPS?
Step 1: DNS
In each domain's DNS settings, create an A record pointing to the same IP:
coffee.example A 203.0.113.10
butcher.example A 203.0.113.10
auto.example A 203.0.113.10(Also www. versions, as CNAMEs to the main name.) Now all three names lead browsers to the same server.
Step 2: Name-Based Virtual Hosting
How does the server know which site to show? Every HTTP/1.1 request includes a Host header with the domain the user typed:
GET / HTTP/1.1
Host: butcher.exampleA web server or reverse proxy (Nginx, Apache, Caddy) uses it to pick the site:
server {
listen 80;
server_name coffee.example www.coffee.example;
root /var/www/coffee;
}
server {
listen 80;
server_name butcher.example www.butcher.example;
root /var/www/butcher;
}
server {
listen 80;
server_name auto.example www.auto.example;
location / { proxy_pass http://127.0.0.1:3001; } # a dynamic app on a local port
}Static sites are served from their own folders. Dynamic sites run as separate processes on different local ports (3001, 3002...) and Nginx reverse-proxies to them.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
B["Browser: butcher.example"] -->|"DNS: 203.0.113.10"| NG["Nginx on :80/:443 - picks site by Host/SNI"]
NG --> S1["coffee site - /var/www/coffee"]
NG --> S2["butcher site - /var/www/butcher"]
NG --> S3["auto app - localhost:3001"]Deep Dive — HTTPS for three domains on one IP addressDeep dive
Name-based virtual hosting works over plain HTTP because the server reads the Host header. With HTTPS there is a chicken-and-egg problem: the certificate must be presented before that header can be decrypted.
One certificate for the server's own hostname
Install a certificate for server1.example.net and serve all three sites from it.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
B["Browser visits coffeeshop.com"] --> TLS["TLS handshake"]
TLS --> CERT["Server presents cert for server1.example.net"]
CERT --> MISMATCH["Name does not match the requested domain"]
MISMATCH --> WARN["Full-page security warning"]
WARN --> LEAVE["Visitors leave - the site is effectively down"]The certificate proves ownership of a name, and it is the wrong name. Browsers treat this as a potential interception, which is exactly what certificate validation is for.
One IP address per domain
Give each site its own IP, bind a listener per address, and serve the matching certificate on each.
This genuinely works and is how it was done before SNI. The cost is IPv4 addresses: they are scarce and billed, three sites need three, and the next customer needs a fourth. The approach also does not scale to a shared host with hundreds of domains, which is the direction this always goes.
SNI, with a certificate per domain
Modern browsers send the requested hostname in the clear during the TLS handshake, as the Server Name Indication extension. The server reads it before choosing a certificate:
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
B["Browser - ClientHello with SNI: butcher.com"] --> S["One IP, one server"]
S --> SEL["Select the certificate matching butcher.com"]
SEL --> HS["Handshake completes - no warning"]
HS --> HOST["Decrypted request - Host header routes to the right vhost"]
LE["Let's Encrypt - one cert per domain, auto-renewed"] --> SEL
R80["Port 80"] --> RED["301 redirect to HTTPS"]- One IP serves any number of domains, each with its own certificate, because the name arrives before the certificate is chosen.
- Free certificates with automatic renewal.
certbot --nginxissues per-domain certificates and renews them on a timer; certificates are short-lived, so automating renewal is not optional. - A SAN certificate listing all the domains is the alternative — one certificate, one renewal — but every domain appears in it, so all three businesses can see each other's names. Per-domain certificates are usually the better default for unrelated customers.
- Redirect port 80 to HTTPS so the plain-HTTP path is not a separate, silently insecure version of each site.
The one caveat worth mentioning: the SNI hostname is sent unencrypted, so a network observer can see which site is being visited even though the traffic is encrypted. Encrypted Client Hello addresses this and is not yet something to depend on.
Isolation and Safety
- Run each app as a separate Linux user or in its own container (Docker), so one compromised site can't read the others' files.
- Separate databases or DB users per site, with resource limits (CPU and memory per container).
- Firewall: only ports 80 and 443 (and SSH restricted to admins) open.
- Separate logs per site. Back up each site.
Growing Later
- More traffic or reliability → put a load balancer in front and run the sites on 2+ servers (the same virtual-host config), with a CDN for static files.
- A managed platform (PaaS) or Kubernetes Ingress does the same Host/SNI-based routing at larger scale.
Wrap-UpWrap-up
Point every domain's DNS A record at the single IP, then run a web server or reverse proxy that uses name-based virtual hosting: the HTTP Host header (and SNI for HTTPS) selects the right site, served from its own folder or proxied to its own local app port. Use per-domain Let's Encrypt certificates with SNI, isolate sites with separate users or containers, and add a load balancer and CDN when it's time to grow.