Skip to content

Fly.io

This guide deploys Doable on Fly.io as three apps (myorg-api, myorg-ws, myorg-web), a shared Postgres cluster, and per-app persistent volumes. The full operator runbook lives in deployment/platforms/fly/DEPLOY.md. The migrate.sh helper script automates everything below in one pass.

Prerequisites

  • flyctl installed and on PATH.
  • A Fly.io account with billing enabled. Shared-cpu machines require it.
  • Doable images are CI-published to ghcr.io/doable-me/doable-{api,ws,web}:latest on every merge to main, so no local build is required.
brew install flyctl                            # macOS
curl -L https://fly.io/install.sh | sh         # Linux or WSL
fly auth login

Architecture on Fly

Internet
  └── myorg-web   (public, fly.dev plus custom domain, port 3000)
        ├── /api/*  rewritten to http://myorg-api.flycast:4000  (private)
        └── /ws/*   rewritten to http://myorg-ws.flycast:4001   (private)

myorg-api ── myorg-postgres (Fly Postgres, private)
myorg-ws  ── myorg-postgres (same cluster, shared)

myorg-api and myorg-ws have no public IP. They are reachable only over Fly's private WireGuard network via .flycast hostnames. The web app's Next.js rewrites proxy /api/* and /ws/* from the browser. Source: deployment/platforms/fly/api.toml, web.toml, ws.toml.

bash deployment/platforms/fly/migrate.sh
# Override region or org if needed:
FLY_REGION=sin FLY_ORG=my-org bash deployment/platforms/fly/migrate.sh

The script creates the three apps, the Postgres cluster, the two volumes, attaches the database, enables vector, pg_trgm, and pgcrypto, generates every secret, mirrors JWT_SECRET and INTERNAL_SECRET to myorg-ws, and prints the bootstrap token. Then jump to the Deploy section.

Manual setup

1. Create the apps

fly apps create myorg-api
fly apps create myorg-ws
fly apps create myorg-web

2. Create Postgres and volumes

fly postgres create --name myorg-postgres --region iad \
  --vm-size shared-cpu-1x --volume-size 10

fly volumes create myorg_api_data --app myorg-api --size 5 --region iad
fly volumes create myorg_ws_data  --app myorg-ws  --size 1 --region iad

3. Attach Postgres

fly postgres attach automatically sets DATABASE_URL as a secret on the target app.

fly postgres attach --app myorg-api myorg-postgres
fly postgres attach --app myorg-ws  myorg-postgres

4. Enable Postgres extensions

echo "CREATE EXTENSION IF NOT EXISTS vector; \
      CREATE EXTENSION IF NOT EXISTS pg_trgm; \
      CREATE EXTENSION IF NOT EXISTS pgcrypto;" \
  | fly postgres connect --app myorg-postgres

5. Required secrets on myorg-api

Generate random values for each. Never reuse them across environments.

fly secrets set --app myorg-api \
  JWT_SECRET=$(openssl rand -hex 32) \
  ENCRYPTION_KEY=$(openssl rand -hex 32) \
  INTERNAL_SECRET=$(openssl rand -hex 32) \
  DOABLE_KEK=$(openssl rand -base64 32) \
  INSTALL_BOOTSTRAP_TOKEN=$(openssl rand -hex 32) \
  INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT=$(date -u -d '+24 hours' +%Y-%m-%dT%H:%M:%SZ) \
  NEXT_PUBLIC_API_URL=https://myorg-web.fly.dev/api \
  NEXT_PUBLIC_WS_URL=wss://myorg-web.fly.dev/ws \
  NEXT_PUBLIC_APP_URL=https://myorg-web.fly.dev \
  CORS_ORIGINS=https://myorg-web.fly.dev

For a custom domain, replace all four URL values and run fly certs add app.example.com --app myorg-web.

6. Mirror shared secrets to myorg-ws

Fly intentionally does not expose secret values across apps. Copy JWT_SECRET and INTERNAL_SECRET to myorg-ws with the exact values you used above:

fly secrets set --app myorg-ws \
  JWT_SECRET=<same value as myorg-api> \
  INTERNAL_SECRET=<same value as myorg-api>

7. Optional AI provider keys

seedAiProviderFromEnv() consumes any of these 19 vars on api boot:

fly secrets set --app myorg-api \
  ANTHROPIC_API_KEY=sk-ant-... \
  OPENAI_API_KEY=sk-... \
  GEMINI_API_KEY=...

Local providers like Ollama, LM Studio, or vLLM do not need an env var. Configure them via their base URL in the setup wizard.

Deploy

fly deploy --app myorg-api --config deployment/platforms/fly/api.toml
fly deploy --app myorg-ws  --config deployment/platforms/fly/ws.toml
fly deploy --app myorg-web --config deployment/platforms/fly/web.toml

The api deploy runs node /app/services/api/dist/db/migrate.js as Fly's release phase before the new machine becomes live (deployment/platforms/fly/api.toml line 14). Watch for the migration output in the log stream.

To pick up freshly published images:

fly deploy --app myorg-api --config deployment/platforms/fly/api.toml --image ghcr.io/doable-me/doable-api:latest
fly deploy --app myorg-ws  --config deployment/platforms/fly/ws.toml  --image ghcr.io/doable-me/doable-ws:latest
fly deploy --app myorg-web --config deployment/platforms/fly/web.toml --image ghcr.io/doable-me/doable-web:latest

Smoke test

curl -i https://myorg-web.fly.dev/
curl -i https://myorg-web.fly.dev/api/health

fly ssh console --app myorg-web
# Inside the SSH console:
curl http://myorg-api.flycast:4000/health
curl http://myorg-ws.flycast:4001/health

Then open https://myorg-web.fly.dev/auth/register in a browser and sign up. The first account becomes the platform owner.

Gotchas

Issue Fix
.flycast does not resolve from your laptop .flycast only works machine-to-machine over Fly's WireGuard mesh. Use fly ssh console for internal curl tests.
Slow first request Expected. auto_stop_machines = "stop" puts machines to sleep. min_machines_running = 1 keeps one warm at all times.
Volume region mismatch Volumes are pinned per region. Add per-region volumes before scaling out.
Cross-app secret sharing Fly does not expose secret values across apps. Always set JWT_SECRET and INTERNAL_SECRET on both api and ws with identical values.
INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT expired Regenerate the token and bump the expiry: fly secrets set --app myorg-api INSTALL_BOOTSTRAP_TOKEN=$(openssl rand -hex 32) INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT=$(date -u -d '+24 hours' +%Y-%m-%dT%H:%M:%SZ)
Migration fails on release fly releases --app myorg-api plus fly logs --app myorg-api for the release phase output. Fix the migration, push a new image, redeploy.

Cost estimate (idle)

Resource Approximate price
3 shared-cpu-1x machines ~$5.82/mo
Fly Postgres shared-cpu-1x ~$1.94/mo
3 volumes (5 GB + 1 GB + default) ~$0.60/mo
Total idle ~$8 to $10/mo

Traffic adds egress at $0.02/GB outbound after 160 GB free.

Reference

  • deployment/platforms/fly/DEPLOY.md (full operator runbook)
  • deployment/platforms/fly/api.toml, web.toml, ws.toml
  • deployment/platforms/fly/migrate.sh
  • Related: Custom Domains, Scaling