Skip to content

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

brew install flyctl
flyctl auth login

2. Create the Fly app

From the repo root (where fly.toml lives):

flyctl apps create lis-bench --org personal

⚠️ Do NOT run flyctl launch. This repo already ships a committed fly.toml (with app = "lis-bench", Singapore region, 512 MB VM, /__health check). flyctl launch would overwrite it with a generic config that's missing the app field — which surfaces later as Error: the config for your app is missing an app name. Use flyctl 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:

flyctl secrets list -a lis-bench

4. First deploy (manual, to confirm everything works)

flyctl deploy --remote-only -a lis-bench

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:

flyctl open

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:

flyctl certs create bench.libearden.dev
flyctl certs show bench.libearden.dev

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:

flyctl tokens create deploy

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:

git push origin 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

flyctl logs

One-off shell on the running container

flyctl ssh console

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" in fly.toml means 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" in fly.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 1 ceiling 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_SECRET set 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 /__health is shallow on purpose)

See also