Proxies·6 min read·

Host-Based Routing: Multiple Apps on One Server

Run several domain-name apps on a single VPS by routing on the Host header with Nginx server blocks or Caddy host matchers, without any port tricks.

NB

Netbay Engineering

Netbay Engineering

On this page

One VPS, four domains, five apps, one public IP. That is the everyday target of host-based routing: the proxy looks at the Host header of each request, matches it to a named configuration, and forwards to the right local port. Clients never see the ports, certificates, or the fact that anything shares the box. This post shows the Nginx and Caddy variants of the same idea.

The Host header is the switch

Every HTTP/1.1 request carries a Host header stating which virtual host it wants. The proxy's job is to treat that header as a route selector. The upstream app receives the request on its own port and has no idea which domain triggered the match — unless you forward the original Host, which you usually should, so the app can generate absolute URLs correctly.

Two things must be true for this to work. The domain names must resolve to your server's IP, and your proxy must listen on the standard ports so browsers find you. After that, adding an app is a new config block, not a new address.

Nginx: one server block per domain

Each domain gets its own server block that matches exactly one Host value and proxies to exactly one port.

nginx
server {
    listen 80;
    server_name api.green.example.com;
    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

server {
    listen 80;
    server_name www.green.example.com green.example.com;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
    }
}

server_name can list several names; the first is the canonical one, and requests for either resolve to the same block. Because blocks are checked in order and the match is exact, there is no ambiguity: the API host goes to the API port and the marketing host to the web port. Any unknown Host is handled by the default server, which you should point somewhere harmless.

The catch-all default block

Anything that does not match a known server_name lands in the default server. Give it a boring response instead of letting it inherit your most important app.

nginx
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

Returning 444 closes the connection without a response, which is ideal for scanners and mailers probing by IP rather than by name. The default_server keyword guarantees this block wins every unhandled tie, including bare-IP requests.

Caddy: host matchers instead of blocks

Caddy encodes the same routing as a filename-like address on each site block, with automatic HTTPS for each domain you list.

caddy
api.green.example.com {
    reverse_proxy 127.0.0.1:3001
}

green.example.com, www.green.example.com {
    reverse_proxy 127.0.0.1:8080
}

Each hostname gets its own certificate from Caddy automatically, and the shared domain pair routes to the web app. Route selection is by matching the request Host against the block address, the same principle as Nginx with less ceremony. When two Caddy blocks could match, Caddy falls back to the order they appear; keep the most specific hostnames first.

Test the routing

bash
curl -sI -H "Host: api.green.example.com" http://127.0.0.1/ | head -n 3
curl -sI -H "Host: green.example.com" http://127.0.0.1/ | head -n 3
curl -s -o /dev/null -w "%{http_code}
" http://127.0.0.1/ --resolve unknown.example.com:80:127.0.0.1

The first two lines prove each Host header reaches the expected upstream. The third line lands on the catch-all, and you want to see the closing status code you configured — not a real app answering on a host nobody owns.

Host header drives the route api.green.example.com www.green.example.com Proxy on :80/:443 reads Host header routes per name API app :3001 Web app :8080

Takeaway

Host-based routing is how one VPS carries many products without port numbers leaking into URLs. Match on the Host header, keep a default_server that answers nothing, and each app stays bound to localhost behind the one public face. Start consolidating your app sprawl onto a single Netbay instance today — more than enough headroom for the pattern, at netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance