Sync to Fly.io
Fly injects an app's secrets into a Machine when it boots, so there is no
earlier point at which seekrit could supply them. A binding owns one Fly app's
secret set — the same slot fly secrets set writes.
| At a glance | |
|---|---|
| What seekrit writes | An app's secrets, delivered to Machines as environment variables |
| Addressed by | The Fly app name — one secret set per app, shared by every Machine in every region |
| Connection carries | The token alone. No account, team, region, or token kind |
| Token permission | A deploy token scoped to one app (fly tokens create deploy), or an org token |
| Takes effect | Next Machine boot. Existing Machines keep their values until fly secrets deploy |
| Value visibility | Write-only at Fly — fly secrets list shows names and digests, not values |
New to sync? Read Third-party sync first — the decryption grant, name mapping, deletions, and failure handling are the same on every destination.
1. Create a token
fly tokens create deploy -a storefront-production
That mints a token scoped to a single app, which is enough to write its secrets
and the least-privilege choice when a connection serves one destination.
fly tokens org covers every app in an organization — reach for it only when one
connection genuinely serves many apps.
Paste it whole, FlyV1 prefix included. Fly's two token shapes travel under
different auth schemes, and seekrit picks the right one by reading the token — so
there is nothing else to configure here. No account, team, or region, and no
token kind to state the way Railway needs one:
printf '%s' "$FLY_API_TOKEN" | seekrit sync connect \
--name acme-fly --provider fly
2. Bind an environment
A Fly app has one secret set, shared by every Machine in every region, so the app name is the whole destination — there are no targets to pick. Fly's own convention is that staging and production are separate apps, and that is the split a binding maps onto:
seekrit sync verify acme-fly --provider fly --fly-app storefront-production
seekrit sync enable --connection acme-fly --provider fly \
--fly-app storefront-production \
--app storefront --env production --acknowledge-decryption
--fly-app is Fly's app; --app is the seekrit application the environment
belongs to. They are different things, which is why the flag is not just
--app.
Verify fetches the app (GET /v1/apps/{app}) rather than reading its secrets —
the same call flyctl makes on every deploy, so every token flavour that can
push is known to be allowed it. A verify stricter than the push would reject
setups that work. A token with read but not write access surfaces on the first
push, in the run ledger, per name.
Pushed secrets are staged until Machines restart. Fly injects secrets when
a Machine boots, so a push updates the app's secret set while already running
Machines keep the values they started with. Machines created after the push have
them; existing ones pick them up on
fly secrets deploy -a storefront-production, or on any deploy.
seekrit will not roll your Machines for you. Rolling a Machine set is a
deployment — a lease per Machine, each one's config resubmitted, health checks
between batches — and a secrets push is the wrong place to be doing it. The
failure mode is an app taken down to deliver an environment variable. A bare
Machine restart is not a substitute either: it carries no secrets version,
which is why flyctl itself does not use one here.
How a push behaves
- One request for the whole environment.
POST /v1/apps/{app}/secretstakes a single{ values: { NAME: value } }map of up to 100 names, applied as a merge: a name with a value is set, a name withnullis unset, and a name that is absent is left alone. - Merge semantics are what make this safe to point at an app that already
has secrets. Fly's own
FLY_*secrets, another binding's names, and anything you set by hand survive a push untouched, and a removal unsets just that name. - Sets and removals travel separately, so a rejected removal cannot report landed secrets as failures.
- A run is capped at 100 requests, small on purpose: Fly's Machines API allows roughly one request per second per action, bursting to three. Since a normal environment is one or two calls, that ceiling only bounds something pathological.
- Fly never sees a value it can echo back. Error text is built from Fly's own message fields, never from a request body.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Rate-limited on Test connection | Fly allows about one request a second | Wait a moment and try again. seekrit tells this apart from a refusal, because the advice is the opposite |
401 on every name | The token was revoked, or it is an app token for a different app | Mint a new token with fly tokens create deploy -a <app> |
404 on the app | The app name is wrong, or the app was deleted | App names are lowercase letters, numbers, and dashes. fly apps list prints the exact one |
| Verify passes, pushes fail | A read-scoped token — verify uses the read call flyctl makes on deploy | Use a deploy or org token |
| Secrets pushed, app still on the old values | Fly injects at Machine boot | fly secrets deploy -a <app>, or deploy. seekrit deliberately rolls nothing |
Run reported partial, 429 in the error | Fly rate-limited the token | Nothing to do — seekrit honors Retry-After and retries the rest |
See also
- Third-party sync — the shared model, naming and filtering, deletions
- seekrit-run launcher — if you control the container's entrypoint, prefer this: no plaintext leaves your side
- CLI reference — every
seekrit syncflag