guide · computers

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.

July 14, 2026 · By Alastair Fraser

A friendly retro robot at a Cloudflare Tunnel switchboard, four green hostname toggles lit up and a catch-all lever pointing to a 404 sign.

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:

HostnameWhat it doesLocal service
agenticbotsitter.comPrimary sitehttp://127.0.0.1:8793
www.agenticbotsitter.comApex aliashttp://127.0.0.1:8793
agenticbotsitting.comTypo’d alias (301 → primary at the edge)http://127.0.0.1:8793
www.agenticbotsitting.comTypo’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:

  1. Creates a CNAME record in the zone pointing <hostname><tunnel-uuid>.cfargotunnel.com.
  2. 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:

  1. Order matters. Ingress is first-match-wins. Put more-specific hostnames above any wildcard.
  2. One service per hostname. If agenticbotsitter.com and agenticbotsitting.com should serve the same site, they need two separate hostname: lines (Cloudflare’s edge only knows about the CNAME, not that two DNS names are “the same site”).
  3. The catch-all goes last. The unnamed - service: http_status:404 line 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.

StatusMeaningWhere to look
200WorkingYou’re done
301 / 302Edge redirect ruleCloudflare dashboard → Rules
404 from edgeHostname not in tunnel ingress (catch-all firing)Add to /etc/cloudflared/config.yml
502CF reached the tunnel but the tunnel can’t reach the local serviceLocal service is down or port is wrong
521WebSocket dead — origin refusedSame 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.yml without also running cloudflared tunnel route dns first. 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:404 last 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 cloudflared restart 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 cloudflared or cloudflared tunnel run my-tunnel to 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:

#CommandWhat it proves
1dig +short <host>DNS resolves via tunnel CNAME
2curl -sS -I https://<host>/ (twice)Edge serves the service; second hit confirms it’s not a 404 drain
3cloudflared tunnel info my-tunnelTunnel is bound and connected to all connectors
4systemctl status cloudflaredThe daemon is up after any config edit
5curl -sS https://<host>/healthz against all 4 hostnamesNone 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

Last verified: 2026-07-14 against Cloudflare Tunnel config running 4 hostnames on this VPS.

Sources

#cloudflare#tunnel#multi-hostname#dns#routing#ingress

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.

Your email address will not be published. Required fields are marked.