Adding custom domains to a Fly.io app centers on one command: fly certs add. Fly requests a Let's Encrypt certificate for the hostname and tells you exactly which DNS records the customer needs. Here is the full DIY loop and the managed alternative when you outgrow it.
# Ask Fly to issue a cert for the customer hostname
fly certs add app.customer.com -a your-app
# Get the exact records + validation status
fly certs show app.customer.com -a your-appStep 1 — Request the certificate
fly certs add registers the hostname with your app and kicks off issuance. Nothing serves yet — you need DNS pointing at Fly first.
Step 2 — Set the DNS records fly certs show tells you
Do not guess the records — fly certs show prints the precise values. For a subdomain the simplest path is a CNAME to your app; for an apex you use A/AAAA plus an _acme-challenge CNAME so Fly can validate and renew:
# subdomain (simplest)
app.customer.com. CNAME your-app.fly.dev.
# apex + ACME validation (values come from 'fly certs show')
customer.com. A <shared-or-dedicated-ipv4>
customer.com. AAAA <ipv6>
_acme-challenge.customer.com. CNAME customer.com.<hash>.flydns.net.The apex needs A/AAAA because of the CNAME-at-apex problem. Confirm the records resolve with the CNAME checker and SSL checker.
Step 3 — Shared vs dedicated IPs
A shared IPv4 is fine for HTTPS: Fly's edge routes by SNI, so many apps share one address. Allocate a dedicated IPv4 only when a customer needs a plain A record to a stable, non-shared IP, or for non-HTTP traffic:
fly ips allocate-v4 -a your-app # dedicated (uses your quota)
# IPv6 is allocated by default and freeStep 4 — Route by host
const host = req.headers.get('host') ?? '';
const tenant = await getTenantByDomain(host);
if (!tenant) return new Response('Unknown domain', { status: 404 }); // fail closedFly sits in front of your app, so forward and trust the client IP the same way you would behind any proxy — see the framework guides like Node/Express for the trust proxy details.
Where the DIY path strains
Each domain is its own fly certs add plus DNS plus a status poll. At a few hundred customers you are scripting issuance, handling retries, and bumping into Let's Encrypt rate limits — the exact problems we detail in how we run SSL for 5,000+ domains. Compare building it yourself in cnames.dev vs self-hosting Caddy on-demand TLS.
The one-API-call way
curl -X POST https://api.cnames.dev/v1/domains \
-H "Authorization: Bearer sk_live_..." \
-d '{"hostname":"app.customer.com","origin":"your-app.fly.dev"}'cnames.dev issues and renews certificates, handles rate-limit-aware retries, and forwards the real client IP to your Fly app. The full architecture is in the complete guide.