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}:lateston every merge to main, so no local build is required.
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.
One-shot script (recommended)¶
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¶
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.tomldeployment/platforms/fly/migrate.sh- Related: Custom Domains, Scaling