feat(deploy): one-command Fly.io launch + fix null client IPs on Fly - #58
Merged
Conversation
Adds deploy/fly-launch.sh — provisions the app, a Managed Postgres
cluster, secrets and the first admin key, then deploys, in one command:
bash deploy/fly-launch.sh --app my-mantis --region iad
Fly has no embeddable "Deploy to Fly.io" button (their one-click is the
dashboard Launch UI), so a single idempotent script plus an opt-in
GitHub Actions workflow is the closest real equivalent.
BUG FIX — the existing fly.toml.example loses every client IP.
docker/Dockerfile sets NODE_ENV=production, and in production mantis
distrusts forwarding headers unless TRUST_PROXY_HEADERS=1. Verified
against the production image: every hit recorded ip = null, and the
supplied X-Forwarded-For was ignored entirely. For a tripwire service
that silently discards the one field that matters, and the per-IP login
limiter fails open with nothing to bucket on. [env] now sets it.
Also pins TRUSTED_IP_HEADER=x-forwarded-for: Fly does not strip an
inbound CF-Connecting-IP and mantis tries that header first, so without
the pin a client can forge its own recorded IP with one header. Verified
all three cases against the image — normal XFF records the real IP, a
forged CF-Connecting-IP is ignored, and a prepended XFF entry loses to
the real rightmost hop. Documented the Cloudflare-fronted variant, which
needs cf-connecting-ip instead.
Script safety properties:
- write-once pepper: refuses to overwrite MANTIS_API_KEY_PEPPER, since
rotating it invalidates every API key ever minted
- idempotent: re-runs skip existing app/cluster/secrets and just redeploy
- secrets go through `fly secrets import` (stdin), never argv or history
- generates BOOTSTRAP_API_KEY itself so the admin key is printed directly
rather than scraped from first-boot logs
- --dry-run prints every command without touching anything
- bash 3.2 compatible (macOS stock bash)
Docs updated to `fly mpg` (Managed Postgres); the older `fly postgres`
path is unmanaged and not covered by Fly support, so it is now opt-in
via --db unmanaged with a warning.
.github/workflows/fly-deploy.yml is inert until FLY_DEPLOY_ENABLED=true,
refuses to deploy a fly.toml missing TRUST_PROXY_HEADERS, and polls
/api/health for db:ok after deploying.
Verified: script logic exercised end-to-end against a stubbed flyctl
(dry-run, real run, re-run idempotency, error paths); generated fly.toml
parses as TOML with the right app/region/base-URL; a script-format
BOOTSTRAP_API_KEY seeds and authenticates against a real mantis instance
(200 with the key, 401 without); both workflow action SHAs verified to
exist against the GitHub API.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bash deploy/fly-launch.sh --app my-mantis --region iadnow provisions the Fly app, a Managed Postgres cluster, the secrets and the first admin key, then deploys — in one command.Fly has no embeddable "Deploy to Fly.io" button (unlike Heroku/Render); their one-click is the dashboard Launch UI, which still needs the app and database to exist. So the closest real equivalent is one idempotent script plus an opt-in Actions workflow for zero-click redeploys.
🐛 The existing Fly example loses every client IP
docker/DockerfilesetsNODE_ENV=production, and in production mantis distrusts forwarding headers unlessTRUST_PROXY_HEADERS=1. The currentfly.toml.examplenever sets it. Verified against the actual production image:The container even warns about it on boot. For a tripwire service this silently discards the single field that matters, and the per-IP login limiter fails open because it has no IP to bucket on.
[env]now setsTRUST_PROXY_HEADERS=1and pinsTRUSTED_IP_HEADER=x-forwarded-for— Fly does not strip an inboundCF-Connecting-IP, and mantis tries that header first, so without the pin a client can forge its own recorded IP with one request header. All three cases verified against the image:X-Forwarded-For(Fly's normal case)203.0.113.77NoneCF-Connecting-IP: 6.6.6.6203.0.113.88XFF: 1.2.3.4, <real>203.0.113.99The Cloudflare-fronted variant is documented inline: front it with a Cloudflare-proxied domain and you must switch to
cf-connecting-ip, or you record Cloudflare's edge IP instead of your visitor's.Script safety properties
MANTIS_API_KEY_PEPPER; rotating it invalidates every API key ever mintedfly secrets import, never argv or shell historyBOOTSTRAP_API_KEYitself instead of scraping first-boot logs--dry-run— prints every command without creating anythingPostgres
Docs move to
fly mpg(Managed Postgres), Fly's supported product. The olderfly postgresbuilds an unmanaged cluster Fly support explicitly does not cover, so it is now opt-in via--db unmanagedwith a warning.--db external(Neon/Supabase/your own) and--db noneare also supported.Continuous deploy
.github/workflows/fly-deploy.ymlis inert until you setFLY_DEPLOY_ENABLED=true, so forks and unconfigured clones never attempt or fail a deploy. It refuses to deploy afly.tomlmissingTRUST_PROXY_HEADERS, and polls/api/healthfordb:okafter deploying.Verification
flyctl isn't installed on this machine, so no live Fly deploy was performed. Everything else was exercised for real:
flyctl: dry-run, real run, re-run idempotency (pepper not rotated, cluster/app skipped), and every error pathfly.tomlparses as TOML with the correct app / region / base URL and both proxy varsBOOTSTRAP_API_KEYseeds and authenticates against a real mantis container on a fresh database —seeded bootstrap admin key from BOOTSTRAP_API_KEY, HTTP 200 with the key, 401 withoutsetup-flyctldid not exist — replaced with the verified1.6commit)🤖 Generated with Claude Code