Fly tunnel
Our Fly APIs are not on the public internet like a classic open-port server—no [http_service], no customer-facing *.fly.dev URL. Traffic arrives through a Cloudflare Tunnel; each app registers as a connector.
If you are not technical: Fly runs the app in a private room; Cloudflare is the reception desk that sends visitors to the right room.
What you will not see on Fly
Section titled “What you will not see on Fly”For a standard Brytebridge API:
| On Fly | Our pattern |
|---|---|
[http_service] in fly.*.toml | Omitted — no public Fly HTTP service |
| Public Fly hostname for API traffic | Not used for staging / UAT / production |
| Open ingress the internet dials directly | No — inbound API traffic does not land on Fly’s edge |
The container still runs the API on port 8080 inside Fly’s private network (http://<fly-app-name>.internal:8080). That port is for health checks, metrics, and for Cloudflare to reach the app—not for browsers to call something.fly.dev in production.
One dedicated tunnel per Fly organization
Section titled “One dedicated tunnel per Fly organization”We run three Fly organizations (staging, UAT, production). Each org has its own Cloudflare Tunnel—not one global tunnel mixed across tiers.
Staging Fly org
Staging tunnel in Cloudflare
Staging apps in this org register here
UAT Fly org
UAT tunnel in Cloudflare
UAT apps in this org register here
Production Fly org
Production tunnel in Cloudflare
Production apps in this org register here
That keeps staging traffic out of production tunnels and makes access control auditable per environment.
How each app registers
Section titled “How each app registers”Every API container runs cloudflared alongside the app (usually via scripts/start-with-tunnel.sh). On startup it uses a tunnel token to connect outbound to Cloudflare—so Fly never needs a public listener for the API.
- Platform maintains the org’s tunnel in Cloudflare (Zero Trust → Networks → Tunnels).
- Cloudflare gets a public hostname row per API (e.g.
api-portal…,s-api-portal…) pointing athttp://<fly-app-name>.internal:8080. - Each Fly app gets
CLOUDFLARE_TUNNEL_TOKENas a secret—the app registers as a connector for that org’s tunnel. - DNS sends the public hostname to Cloudflare (proxied), not to Fly.
Use the repo / Fly app name consistently so hostname → app mapping is obvious (see Cloudflare naming).
Required on each Fly app
- No
[http_service]infly.staging.toml/fly.uat.toml/fly.main.toml CLOUDFLARE_TUNNEL_TOKENset withfly secrets set(see [Fly checklist](/fly-checklist/) — Fly environment variables)/healthand/metricson port 8080- Matching **public hostname** row in the **same tier’s** tunnel
How traffic flows
Section titled “How traffic flows”User / partner → public hostname (Cloudflare DNS + TLS + WAF) → org-specific Cloudflare Tunnel → connector for that Fly app (cloudflared) → http://<fly-app-name>.internal:8080Cloudflare owns the front door. Fly runs the process. The tunnel is the only supported path for routine API access in staging, UAT, and production.
Cloudflare vs Fly responsibilities
Section titled “Cloudflare vs Fly responsibilities”| Piece | Where it lives |
|---|---|
| Tunnel definition, hostnames, DNS, access rules | Cloudflare |
Container, cloudflared, token, no public service | Fly.io + this page |
| Deploy branch → Fly app | GitHub sacred branches |
Related
Section titled “Related”- Fly.io — orgs, logging, metrics, deploy
- Cloudflare — hostnames and public URLs
- Fly checklist — tier promotion +
CLOUDFLARE_TUNNEL_TOKEN - Zero Trust — why we avoid open ingress