Deploy to bench.libearden.dev¶
Production deploy path: GitHub → Fly.io (Singapore) → Cloudflare DNS (libearden.dev).
What you'll do once¶
These steps run once at deploy-bootstrap. Subsequent deploys are automated — push to main → CI runs tests → Fly deploys.
1. Install flyctl + sign in¶
2. Create the Fly app¶
From the repo root (where fly.toml lives):
⚠️ Do NOT run
flyctl launch. This repo already ships a committedfly.toml(withapp = "lis-bench", Singapore region, 512 MB VM,/__healthcheck).flyctl launchwould overwrite it with a generic config that's missing theappfield — which surfaces later asError: the config for your app is missing an app name. Useflyctl apps create(it touches Fly's backend only, never the local file).
The app name is in fly.toml (app = "lis-bench"). Pick a different name if lis-bench is taken globally on Fly; update fly.toml accordingly before the create command.
3. Set runtime secrets¶
These are the env vars the app reads at startup. Set them as Fly secrets (encrypted, not in source). Run from the repo root so flyctl reads the app field from fly.toml; passing -a lis-bench explicitly is the belt-and-suspenders version:
# Anthropic
flyctl secrets set -a lis-bench ANTHROPIC_API_KEY="sk-ant-..."
# Supabase — copy from Supabase dashboard → Settings → API
flyctl secrets set -a lis-bench SUPABASE_URL="https://<ref>.supabase.co"
flyctl secrets set -a lis-bench SUPABASE_ANON_KEY="<anon key>"
flyctl secrets set -a lis-bench SUPABASE_SERVICE_ROLE_KEY="<service role key>"
flyctl secrets set -a lis-bench DATABASE_URL="postgresql://postgres.<ref>:<pw>@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres"
# JWT signing secret — Settings → API → JWT Settings → JWT Secret.
# WITHOUT THIS THE APP REFUSES TO AUTH IN PRODUCTION (LIS-280).
flyctl secrets set -a lis-bench SUPABASE_JWT_SECRET="<jwt secret>"
# Optional: Sentry DSN if you want error monitoring
flyctl secrets set -a lis-bench SENTRY_DSN="https://...@sentry.io/..."
If you hit Error: the config for your app is missing an app name: you're either not in the repo root, or fly.toml got clobbered by flyctl launch. Either cd to repo root or restore fly.toml from git checkout fly.toml. The -a lis-bench flag above sidesteps the whole issue.
Verify:
4. First deploy (manual, to confirm everything works)¶
This builds the Docker image on Fly's builder (no local Docker needed) and ships to the Singapore region. First build is ~3 minutes; subsequent builds use cached layers and run ~30 seconds.
When it succeeds, you'll get a *.fly.dev URL. Visit it:
Sign in via GitHub OAuth (the Supabase OAuth callback URL in your Supabase project will need to allow your Fly domain — see Supabase dashboard → Authentication → URL configuration).
5. Custom domain via Cloudflare¶
In Cloudflare's DNS for libearden.dev:
| Type | Name | Value | Proxy |
|---|---|---|---|
CNAME |
bench |
lis-bench.fly.dev |
✅ Proxied (orange cloud) |
Wait ~30 seconds for DNS to propagate, then attach the domain on the Fly side:
Fly issues a LetsEncrypt cert; the proxy resolves through Cloudflare. Once the cert is Ready, https://bench.libearden.dev works.
Update Supabase OAuth callback URLs:
In your Supabase project → Authentication → URL Configuration:
- Site URL: https://bench.libearden.dev
- Redirect URLs: add https://bench.libearden.dev/auth/callback
Otherwise the GitHub OAuth round-trip will land on the wrong host.
6. Add the GitHub Actions deploy token¶
In your local terminal:
Copy the token. In GitHub:
- Repo Settings → Secrets and variables → Actions → New repository secret
- Name: FLY_API_TOKEN
- Value: (paste)
Now git push origin main triggers .github/workflows/deploy.yml → tests → deploy. No more manual flyctl deploy.
What you'll do regularly¶
Deploy a change¶
Just push to main:
GitHub Actions runs pytest tests/unit/. If green, flyctl deploy --remote-only ships. ~2 minutes end-to-end on warm builds.
Roll back¶
flyctl releases # find the previous release tag
flyctl image rollback <ver> # ship the previous image
Tail logs¶
One-off shell on the running container¶
Toggle diagnostics temporarily¶
flyctl secrets set LIS_DIAGNOSTICS_ENABLED=1
# (debug whatever you need; visit /__sentry/test etc.)
flyctl secrets set LIS_DIAGNOSTICS_ENABLED=0
Each secrets set triggers a redeploy automatically.
Operational notes¶
- Sleeping instances:
auto_stop_machines = "stop"infly.tomlmeans the machine spins down to 0 when idle. First request after idle takes ~3 seconds. Acceptable for single-user; not for shared/public traffic — flip to"suspend"for faster wake-up at the cost of always-on memory billing. - Region:
primary_region = "sin"(Singapore) matches the Supabase region. Cross-region DB queries from another region would tank latency. - Memory: 512MB. Torch is the heaviest import. If you see OOM kills in logs, bump
memory = "1024mb"infly.toml. - Concurrency:
soft_limit = 50, hard_limit = 100. Single-user platform; these are way above realistic load. Lower if scaling matters later. - Cost: Fly's free tier covers small apps. Singapore region + custom domain + occasional usage should run ~$2-5/month. Set a
flyctl scale count 1ceiling so you never accidentally fan out.
When deploys fail¶
| Symptom | Likely cause | Fix |
|---|---|---|
flyctl deploy exits with build error |
Missing system dep in Dockerfile | Check apt-get install line; rebuild |
| App boots but every request → /login redirect loop | SUPABASE_JWT_SECRET wrong/missing |
flyctl secrets list; re-set; redeploy |
| OAuth round-trip lands on wrong host | Supabase callback URL not updated | Authentication → URL config |
/__health 200s but / 500s |
DB connection failing | Check DATABASE_URL; verify Supabase IP allow list |
| Cloudflare shows 522 | Fly app sleeping; LB timing out | Bump Cloudflare timeout, or set min_machines_running = 1 |
| LetsEncrypt cert not issuing | Cloudflare proxy not orange-clouded | Toggle proxy on; retry flyctl certs show |
Hardening checklist (LIS-280 prerequisites)¶
Don't deploy until each is satisfied. These all ship in the same PR as the Dockerfile (see LIS-280 hardening):
- JWT signature verification (
SUPABASE_JWT_SECRETset as a Fly secret) - Rate limiting on LLM endpoints (slowapi)
- Security headers middleware (CSP, HSTS, X-Frame-Options, etc.)
- Diagnostic-route gating (default-off in prod via
SENTRY_ENVIRONMENT=prod) - Anthropic spend cap helpers (per-service enforcement is a follow-up)
- Per-service spend-cap enforcement (deferred — currently only the helpers are in place)
- Sentry release tagging in CI (so each deploy creates a Sentry release tag)
- Health-check deeper readiness probe (current
/__healthis shallow on purpose)