Deploy to Fly.io
Это содержимое пока не доступно на вашем языке.
The repository ships a working fly.toml. This page explains what is in it and why, so you can
adopt it as is or adapt it with your eyes open.
-
Create the app and pick a region:
Terminal window fly launch --no-deployKeep the shipped
fly.tomlwhen it asks. The sections below depend on what is in it. -
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.
For PostgreSQL, set
POSTGRES_URLinstead and skipDATABASE_NAMEin the next step, since the database name is part of that URL. Set one connection string, never both; see PostgreSQL. -
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
The release command
Section titled “The release command”[deploy] release_command = "sh -c 'bun run db:setup && bun run db:migrate'"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 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. The
first-run setup page at GET /admin still works in that state, so the symptom looks like a login
bug when the real problem is 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 the connection string, DATABASE_NAME on MongoDB, and ISSUER. Yes,
ISSUER too: it is read for the admin client’s redirect_uri. On PostgreSQL the setup step is
db:setup:pg.
db:migrate follows in the same command because the server refuses to start against a database that
is behind the migrations its release declares. Left out, a release carrying its first migration
deploys an image that cannot boot; run from here, the same release is gated on the migration
succeeding. Most releases declare none and the step reports current and exits, but 0.7.0, 0.8.0,
0.10.0 and 0.11.0 each carried migrations that cannot be undone, so read the release notes before a deploy and take a
backup when they name one. The sh -c wrapper is what makes && a shell operator instead of two more arguments to bun.
The health checks
Section titled “The health checks”[[http_service.checks]] method = 'GET' path = '/health' interval = '30s' timeout = '5s' grace_period = '15s' headers = { X-Forwarded-Proto = 'https' }
[[http_service.checks]] method = 'GET' path = '/ready' interval = '60s' timeout = '10s' grace_period = '20s' headers = { X-Forwarded-Proto = 'https' }Boot does a database connection and a keystore load at module scope, so a Machine that cannot reach the datastore exits instead of serving. Without a check the proxy cannot tell that apart from a healthy Machine and routes to it anyway.
Two details are there on purpose. 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 only route the rate limiter never counts — a refused liveness probe would take
the Machine out of the load balancer. It answers for the process alone, which is why the second
check exists. GET /ready reports whether the datastore is reachable, and Fly’s checks stop routing
to a failing Machine without restarting it, so one whose database has gone away is shed instead of
cycled. Its interval is the longer of the two on purpose. The endpoint holds its last answer for a
second and shares one outstanding probe, so it cannot become database load, and there is little to
gain from asking twice a minute about something a deploy cannot fix.
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 = 'off'withauto_start_machines = true: the proxy never stops a Machine for want of traffic, so a request after a quiet night is served by a running process instead of waiting for a boot.min_machines_runningis left out on purpose, since it only means anything while something is allowed to stop Machines. Autostart stays on purely as a backstop: a Machine stopped by a host event comes back on the next request. A deployment nobody depends on yet will probably want the opposite, and that is what the second configuration below is.strategy = 'bluegreen': with a single Machine a rolling deploy replaces it in place, so every deploy costs a boot’s worth of downtime. Blue-green boots the replacement alongside it and moves traffic once the new Machine’s checks pass. It is available only because nothing here has a volume attached; blue-green and canary both refuse one.- 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.
The second configuration
Section titled “The second configuration”The repository also ships fly.conformance.toml, which describes a separate Fly app: the instance
the OpenID conformance suite is pointed at. It is the same file with two answers reversed:
auto_stop_machines = 'stop' and min_machines_running = 0, so it costs close to nothing between
runs, and no blue-green strategy, because a test bed can afford a boot’s downtime.
Deploy it with fly deploy --remote-only -c fly.conformance.toml --ha=false. The last flag is worth
knowing about: on the first deploy of a new app flyctl creates a second Machine for high
availability whatever the config says, and then advises setting min_machines_running = 0 — which
this file already does, because the two settings are unrelated. Left as it lands, a test bed runs two
Machines whose in-memory rate-limit counters are independent, so the effective allowance is double
the configured one.
Two of its choices are probably worth borrowing for any second environment. It sets a different
DATABASE_NAME, so a run cannot write into production data and its console can be configured however
a test plan requires. Server settings are stored per database, so sharing a database would mean
sharing them. And it stops instead of suspending, though suspending resumes faster: boot connects
to the datastore and loads the keystore at module scope, so a resumed process holds a connection pool
it established before it was frozen, and the failure looks like a server defect instead of a cold
start.