Fly.io
Fly.io is essentially Docker for running applications—you ship a container image, Fly runs it close to users, and you manage it with config files and deploy commands.
At Brytebridge we use Fly for APIs: backend services in containers, deployed through GitHub Actions—not Fly’s dashboard auto-deploy.
Three Fly organizations
Section titled “Three Fly organizations”We run three separate Fly organizations—always staging, then UAT, then production:
Staging
Its own Fly org. All staging APIs and services run here.
UAT
Its own Fly org. All UAT workloads run here.
Production
Its own Fly org. All production APIs and services run here.
This is intentional Zero Trust design: segment by environment so each person and token gets the least permissions needed for their layer. Developers work in staging (and UAT where policy allows)—not with blanket access to production—so we avoid inadvertent cross-environment mistakes and security issues.
Security — Fly tunnel (no public Fly ingress)
Section titled “Security — Fly tunnel (no public Fly ingress)”We do not expose APIs with a Fly [http_service], a public Fly hostname, or a visible ingress port for customer traffic. Staging, UAT, and production all use the same pattern unless platform approves an exception.
Each Fly organization has its own dedicated Cloudflare Tunnel. Every app in that org runs cloudflared and registers with a tunnel token—there is no open Fly front door for the API.
Fly tunnel standards — no public services, org-specific tunnels, per-app registration
Dedicated egress IPv4 (only when needed)
Section titled “Dedicated egress IPv4 (only when needed)”Some Fly APIs call an external data layer that allowlists IP addresses (e.g. MongoDB Atlas). Those apps need a dedicated egress IPv4 on the Fly app, allocated and attached manually in Fly—not every API needs one.
- Need it when Mongo or another service requires a fixed outbound IP for access rules.
- Skip it when there is no IP-restricted external datastore or the service does not use IP allowlists.
fly ips allocate-egress --region iad -a <fly-app-name> --yes # once per app that needs itfly ips list -a <fly-app-name> # use type egress for Atlas / allowlists—not ingressWhitelist that egress address in Atlas (or the vendor’s IP access list) per environment (staging / UAT / production app).
Multi-region
Section titled “Multi-region”A goal of running on Fly is multi-region availability without a separate architecture per region. The same Docker image can be deployed with Fly auto-scaling so additional machines spin up where configured—and, when we turn it on, the same app can run in more than one region with Fly routing traffic among them.
Today we standardize on a single primary region:
Starting region: IAD (US East)
New API fly.*.toml files set primary_region = "iad" (Ashburn, East Coast) unless platform directs otherwise.
That keeps deploys predictable while we roll out tunnels, org separation, and CI/CD.
When multi-region matters (latency, redundancy, or capacity), we add regions in the Fly config—e.g. [regions] / additional machines per region in fly.staging.toml, fly.uat.toml, or fly.main.toml. Fly spreads the existing container across those regions; teams do not maintain separate Dockerfiles per coast.
Cloudflare remains the public edge; Fly multi-region is about where the containers run behind the tunnel.
Logging (automatic, org-wide)
Section titled “Logging (automatic, org-wide)”Each Fly organization has its own dedicated log shipper. It collects logs from every app in that org and routes them to Grafana Cloud (Loki)—no per-app wiring and no log-file paths to manage in each repo.
Staging
Org-level log shipper → Grafana Cloud
UAT
Org-level log shipper → Grafana Cloud
Production
Org-level log shipper → Grafana Cloud
What this means for API teams
- You do not need the old app-by-app log shipping setup (custom agents, sidecars, or file paths).
- Write normal stdout / stderr logging; Fly and the org shipper handle the rest at system level.
- Tiers stay separated because each org has its own shipper.
Health and metrics (required on every app)
Section titled “Health and metrics (required on every app)”Every application on Fly must expose /health (liveness/readiness) and /metrics (Prometheus format). Production also requires Sentry for error observability and go-live sign-off.
Node.js standard: prom-client + nstats
Our APIs are almost always Node services. We standardize on:
prom-client— default process metrics on/metricsnstats— Express middleware for HTTP stats; mergenstats.toPrometheus()into/metrics(ignore/healthand/metricsin nstats route lists)
Fastify or other frameworks should still expose the same two paths and Prometheus output.
Org-level metrics collector
Section titled “Org-level metrics collector”Each org runs a dedicated Fly app that scrapes /health and /metrics on the other apps in that org and pushes Prometheus data to Grafana Cloud:
Staging
Fleet monitor app scrapes staging APIs → Grafana Cloud
UAT
Fleet monitor app scrapes UAT APIs → Grafana Cloud
Production
Fleet monitor app scrapes production APIs → Grafana Cloud
API teams do not wire Grafana or Prometheus per service. Your job is /health, /metrics, and the Node modules above. Full stack picture: Grafana standards.
CI/CD and Fly
Section titled “CI/CD and Fly”We deploy APIs to Fly through GitHub Actions, not through Fly’s own “deploy on push” integration on the app.
Do not turn on Fly auto-deploy
Unless platform / tech lead has reviewed it with you, do not enable automatic deployment through Fly (Fly’s GitHub app, deploy-on-merge on the Fly dashboard, or similar). That sidesteps our SDLC audit trail and makes it easy to push to the wrong org or tier by mistake.
GitHub is the deployment process to Fly. Use branch-named workflows (fly-deploy-staging.yml, fly-deploy-uat.yml, fly-deploy-main.yml) that run flyctl deploy --config fly.<branch>.toml with the right org token.
Sacred branches and one Dockerfile
Section titled “Sacred branches and one Dockerfile”Three primary branches—always staging, UAT, production—each with its own fly.*.toml and workflow. One shared Dockerfile per repo; only config and secrets change per tier.
Staging
staging branch
fly.staging.toml
fly-deploy-staging.yml
UAT
uat branch
fly.uat.toml
fly-deploy-uat.yml
Production
main branch
fly.main.toml
fly-deploy-main.yml
Details: Branches · GitHub · Fly checklist
Why Fly.io vs AWS?
We choose Fly for APIs because it gives us gentle guardrails and much lower ops burden than wiring load balancers, cross-region routing, storage, and per-app logging on AWS.
Org-level logs already flow to Grafana Cloud; deploys go through GitHub to tier-separated Fly orgs.