Deploy to Fly.io
The repository ships a working fly.toml. This page explains what is in it and why, so you can
adopt or adapt it deliberately.
-
Create the app and pick a region:
Terminal window fly launch --no-deployKeep the shipped
fly.tomlwhen it asks — the sections below are load-bearing. -
Set the database connection string as a secret:
Terminal window fly secrets set MONGODB_URI='mongodb+srv://user:password@cluster.example.mongodb.net'See MongoDB Atlas for the details of the string, including why the database name does not belong in it.
-
Set the two non-secret variables in
fly.toml:[env]ISSUER = 'https://auth.example.com'DATABASE_NAME = 'OAuth'ISSUERmust be the public URL browsers reach the app at, and it must be right the first time — the release command bakes it into the seeded console client’s redirect URI once. Neither value is a credential, so neither needs to be a secret. -
Deploy:
Terminal window fly deploy --remote-only
release_command = 'bun run db:setup'
Section titled “release_command = 'bun run db:setup'”[deploy] release_command = 'bun run db:setup'This is the most important line in the file. Fly runs it on a temporary Machine with the app’s environment, once per deploy attempt, before any new Machine serves traffic, and aborts the deploy on a non-zero exit.
It has to run before the app serves, not after, because the seeds must exist for the deployment to
be usable at all: without the reserved admin project, bucket and client, GET /admin/login
redirects to /auth with a client_id no Client document matches, and the admin plane is
unreachable — while the first-run setup page at GET /admin still works, which makes it look like a
login bug rather than a missing seed. The same run provisions the TTL and unique-e-mail indexes that
the collections would otherwise be created without.
It also aborts the deploy usefully: db:setup exits non-zero when a constraint it declares is not
in force, so a database whose data violates a new unique index fails the deploy instead of silently
serving without it.
The release command needs MONGODB_URI, DATABASE_NAME and ISSUER — ISSUER included, because
it is read for the admin client’s redirect_uri.
The health check
Section titled “The health check”[[http_service.checks]] method = 'GET' path = '/health' interval = '30s' timeout = '5s' grace_period = '15s' headers = { X-Forwarded-Proto = 'https' }Boot does a MongoDB connection and a keystore load at module scope, so a Machine that cannot reach the datastore exits rather than serving. Without a check the proxy cannot tell that apart from a healthy Machine and routes to it anyway.
Two details are deliberate. The X-Forwarded-Proto = 'https' header is a documented safeguard
against force_https turning the check into a 301 that the proxy reads as a failure. And the
15-second grace period covers a cold start, since auto_start_machines wakes Machines from stopped.
GET /health is the one route the rate limiter never counts — a refused liveness probe would take
the Machine out of the load balancer.
rateLimit.trustedProxy stays on
Section titled “rateLimit.trustedProxy stays on”Fly’s proxy sits in front of the app, so every request arrives from it. Leave
rateLimit.trustedProxy at its default of
true: with it off, every caller is counted as the proxy’s own address, the whole internet shares
one allowance, and all traffic is refused within seconds. The origin resolver reads Fly-Client-IP
first, which is exactly the header Fly sets. Behind a reverse proxy
covers the other direction, where the same default is wrong.
The rest of the file
Section titled “The rest of the file”internal_port = 3000matches theEXPOSE 3000in the Dockerfile and the portbun startlistens on.force_https = true, soISSUERis anhttpsURL and HSTS (which the server emits unconditionally) is not a lie.auto_stop_machines = 'stop',auto_start_machines = true,min_machines_running = 0: fine for a low-traffic deployment, and the health check’s grace period accounts for the cold start.- One shared-CPU VM with 1 GB of memory. Note that rate-limit counting is per instance and held in memory, so scaling to N Machines multiplies the effective allowance by N — see the caveat in Kubernetes, which applies identically here.