Cloudflare Tunnel: Adding Multiple Hostnames Without Breaking the Others
How to put several public hostnames on a single Cloudflare Tunnel: cloudflared tunnel route dns per hostname, ingress rule order, incremental test pattern, and the catch-all 404 fallback.

When one tunnel covers many hostnames
A single cloudflared daemon can route N public hostnames to N local services. The cost is one tunnel UUID; the benefit is one daemon’s logs, one set of credentials, one restart workflow. Today Agentic Bot Sitter runs 4 hostnames on one tunnel UUID, all fronting /var/www/agenticbotsitter/.
This guide assumes you already have a working named tunnel (one daemon, one UUID, one /etc/cloudflared/config.yml). If you don’t, read Cloudflare Tunnel Setup for a Static Site first and come back.
The same tunnel UUID (800a6c54-c905-40a7-beb3-d06212b6cf96 on this VPS) currently serves:
| Hostname | What it does | Local service |
|---|---|---|
agenticbotsitter.com | Primary site | http://127.0.0.1:8793 |
www.agenticbotsitter.com | Apex alias | http://127.0.0.1:8793 |
agenticbotsitting.com | Typo’d alias (301 → primary at the edge) | http://127.0.0.1:8793 |
www.agenticbotsitting.com | Typo’d apex alias (301 → primary at the edge) | http://127.0.0.1:8793 |
All four share the same upstream because they all serve the same webroot. If you wanted agenticbotsitter.com to point at a different local port, you’d just give it its own ingress line — that’s the whole pattern.
The cloudflared tunnel route dns per hostname
For each new hostname, you need Cloudflare’s authoritative DNS to know the tunnel is the destination. The command does both:
cloudflared tunnel route dns my-tunnel my-new-hostname.com
cloudflared tunnel route dns my-tunnel my-other-hostname.com
What this does:
- Creates a CNAME record in the zone pointing
<hostname>→<tunnel-uuid>.cfargotunnel.com. - Registers the hostname in the tunnel’s route table on the Cloudflare side.
It’s idempotent — re-running the same command for an already-routed hostname is a no-op (it just confirms the CNAME still exists).
The full help:
cloudflared tunnel route dns [TUNNEL] [HOSTNAME]
Creates a DNS CNAME record hostname that points to the tunnel.
OPTIONS:
--overwrite-dns, -f Overwrites existing DNS records with this hostname
You’ll only need --overwrite-dns if the hostname already has a different CNAME or an A record you want to replace. For greenfield hostnames, plain route dns is fine.
The ingress rule
In /etc/cloudflared/config.yml, each hostname that should reach a local service needs its own ingress block:
ingress:
- hostname: agenticbotsitter.com
service: http://127.0.0.1:8793
- hostname: www.agenticbotsitter.com
service: http://127.0.0.1:8793
- hostname: agenticbotsitting.com
service: http://127.0.0.1:8793
- hostname: www.agenticbotsitting.com
service: http://127.0.0.1:8793
- service: http_status:404
Three rules to internalise:
- Order matters. Ingress is first-match-wins. Put more-specific hostnames above any wildcard.
- One service per hostname. If
agenticbotsitter.comandagenticbotsitting.comshould serve the same site, they need two separatehostname:lines (Cloudflare’s edge only knows about the CNAME, not that two DNS names are “the same site”). - The catch-all goes last. The unnamed
- service: http_status:404line is the safety net. Any hostname that resolves to this tunnel but doesn’t match a named ingress block gets a clean 404 from the edge instead of leaking the wrong upstream service.
On this VPS the live config also fronts other tunnels-in-the-tunnel (hermes, mockups, bestshirtdesigns, atsresumedesigner) — each its own hostname. The pattern scales the same way: one named block per hostname, catch-all last.
Test each hostname independently
For each new hostname, in this exact order:
# 1. DNS resolves to the tunnel CNAME
dig +short my-new-hostname.com
# 2. Edge serves your service.
# Run curl twice — the first hit can return 404 while the tunnel warms up.
curl -sS -I https://my-new-hostname.com/ | head -1
curl -sS -I https://my-new-hostname.com/ | head -1
# 3. The other hostnames still work (test oldest first)
curl -sS -I https://agenticbotsitter.com/ | head -1
curl -sS -I https://www.agenticbotsitter.com/ | head -1
curl -sS -I https://agenticbotsitting.com/ | head -1
curl -sS -I https://www.agenticbotsitting.com/ | head -1
If hostname N breaks hostname N-1, the ingress rule order is wrong: reorder, restart cloudflared tunnel run my-tunnel (or systemctl restart cloudflared if you’re using systemd), retest. A misordered ingress shows up as one hostname silently getting the upstream of another, never as a clean error — that’s why step 3 matters.
If dig isn’t installed (command not found on this host), use host <hostname> or nslookup <hostname> instead — same answer, same idea.
The cleanest failure mode
A wrong hostname shows up as a 502 from the origin (CF can’t reach the local service) or a 521 (WebSocket dead). Check the cloudflared tunnel logs with cloudflared tunnel info my-tunnel. If the tunnel itself is healthy, the issue is the local service for that hostname, not the tunnel.
The localhost-vs-127.0.0.1 trap
When one hostname on a multi-hostname tunnel returns 502 with a 16-byte error code: 502 body while every other hostname on the same tunnel is fine, the cause is almost always localhost instead of 127.0.0.1 in that one ingress line. Why: most local services (Flask, Gunicorn, etc.) bind to 127.0.0.1 only; cloudflared then resolves localhost via RFC 6724 IPv6 preference and gets a connection-refused that the tunnel silently turns into 502. Fix: change that one ingress line from service: http://localhost:PORT to service: http://127.0.0.1:PORT and restart cloudflared. No dashboard changes needed.
Don’t keep restarting
CF edge POPs (especially PHX) hold stale routing-table views 2–5 min after a tunnel restart. If you add a hostname, restart, and 502 on the new one within 5 min — wait. Probe a known-working hostname on the same tunnel and watch /var/log/cloudflared.log mtime. If the mtime moves, your tunnel saw the requests and the edge just doesn’t have the fresh route yet. If the mtime is idle, the edge is rejecting pre-forward.
| Status | Meaning | Where to look |
|---|---|---|
200 | Working | You’re done |
301 / 302 | Edge redirect rule | Cloudflare dashboard → Rules |
404 from edge | Hostname not in tunnel ingress (catch-all firing) | Add to /etc/cloudflared/config.yml |
502 | CF reached the tunnel but the tunnel can’t reach the local service | Local service is down or port is wrong |
521 | WebSocket dead — origin refused | Same root cause as 502 |
For 502/521, the tunnel itself is healthy. Fix the local service, not the tunnel. The opposite case (tunnel broken) shows up as cloudflared tunnel info my-tunnel reporting the connector as disconnected — fix the daemon first.
What not to do
Four footguns that will cost you an hour each the first time:
- Don’t add a hostname to
/etc/cloudflared/config.ymlwithout also runningcloudflared tunnel route dnsfirst. The tunnel will accept the config, but Cloudflare’s edge has no CNAME pointing at it — the request never reaches the tunnel, and you get a generic Cloudflare error, not a useful one. - Don’t put
- service: http_status:404last if you have a wildcard CNAME (*.example.com) in the zone. The catch-all in the tunnel ingress will eat every wildcard hit before any DNS-side redirect can fire. Either move the wildcard to its own tunnel or give it an explicit hostname entry. - Don’t put the new hostname’s service first in the ingress list. First-match wins; if your new hostname is alphabetically or topologically “first” in the file, the older ones can never match. Put more-specific hostnames on top, catch-all at the bottom.
- Don’t forget
cloudflaredrestart after editing the config. The daemon reads the file at startup; an edit with no restart means the running process still has the old ingress table.systemctl restart cloudflaredorcloudflared tunnel run my-tunnelto pick up the change.
One more quiet one: don’t trust the dashboard-published config blindly. On this VPS, /etc/cloudflared/config.yml is decorative — Cloudflare’s zero-trust dashboard publishes the authoritative ingress to the running process. The file is kept in sync for documentation only. If you edit the file and nothing happens, the dashboard is the source of truth. See /opt/data/skills/devops/cloudflare-tunnel-deploy/SKILL.md for the full sync workflow.
Verification checklist
Before declaring the new hostname done, run all five:
| # | Command | What it proves |
|---|---|---|
| 1 | dig +short <host> | DNS resolves via tunnel CNAME |
| 2 | curl -sS -I https://<host>/ (twice) | Edge serves the service; second hit confirms it’s not a 404 drain |
| 3 | cloudflared tunnel info my-tunnel | Tunnel is bound and connected to all connectors |
| 4 | systemctl status cloudflared | The daemon is up after any config edit |
| 5 | curl -sS https://<host>/healthz against all 4 hostnames | None of the older hostnames were broken by the new ingress block |
If all five pass, the new hostname is live and the existing ones are intact. Anything else, see Cloudflare Tunnel Troubleshooting.
Sources
- Cloudflare Tunnel public hostname orchestration
- ABS companion: Cloudflare Tunnel Setup for a Static Site
- ABS companion: Cloudflare Tunnel Troubleshooting
Last verified: 2026-07-14 against Cloudflare Tunnel config running 4 hostnames on this VPS.


Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.