Skip to content

← Back to Fly.io standards

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.


For a standard Brytebridge API:

On FlyOur pattern
[http_service] in fly.*.tomlOmitted — no public Fly HTTP service
Public Fly hostname for API trafficNot used for staging / UAT / production
Open ingress the internet dials directlyNo — 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.

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.

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.

  1. Platform maintains the org’s tunnel in Cloudflare (Zero Trust → Networks → Tunnels).
  2. Cloudflare gets a public hostname row per API (e.g. api-portal…, s-api-portal…) pointing at http://<fly-app-name>.internal:8080.
  3. Each Fly app gets CLOUDFLARE_TUNNEL_TOKEN as a secret—the app registers as a connector for that org’s tunnel.
  4. 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] in fly.staging.toml / fly.uat.toml / fly.main.toml
  • CLOUDFLARE_TUNNEL_TOKEN set with fly secrets set (see [Fly checklist](/fly-checklist/) — Fly environment variables)
  • /health and /metrics on port 8080
  • Matching **public hostname** row in the **same tier’s** tunnel
User / partner
→ public hostname (Cloudflare DNS + TLS + WAF)
→ org-specific Cloudflare Tunnel
→ connector for that Fly app (cloudflared)
→ http://<fly-app-name>.internal:8080

Cloudflare owns the front door. Fly runs the process. The tunnel is the only supported path for routine API access in staging, UAT, and production.

PieceWhere it lives
Tunnel definition, hostnames, DNS, access rulesCloudflare
Container, cloudflared, token, no public serviceFly.io + this page
Deploy branch → Fly appGitHub sacred branches