Deploying the app (Fly.io)¶
The app runs on Fly.io as a single VM in lhr (London) with a 1 GB volume mounted at /data for SQLite + uploaded media.
The Fly account is personal (olly.willans@gmail.com) — not the Torchbox account. Always confirm before deploying:
If it doesn't say olly.willans@gmail.com, run fly auth logout && fly auth login and pick the right account.
How deploys happen¶
GitHub Actions runs the deploy on every push to main. The workflow file is .github/workflows/deploy.yml. It:
- Runs the test suite.
- Builds the Docker image (multi-stage; Pillow + zlib at build time, smaller libjpeg at runtime).
- Pushes to Fly.
You don't normally run fly deploy by hand — merge to main, wait ~2 minutes, the new build is live.
Migrations¶
Migrations run on container boot via entrypoint.sh, not via Fly's release_command. The reason matters: release_command runs on a separate ephemeral machine that doesn't mount the SQLite volume, so manage.py migrate there ran against an empty DB and silently no-op'd in prod for ages. We caught it the third time it bit us.
So: on every machine boot, entrypoint.sh runs python manage.py migrate --noinput against the volume-backed DB, then exec gunicorn. Single-machine deploy means no race; migrate is idempotent.
If you ever need to migrate manually:
First-time setup (if you're forking)¶
brew install flyctl # macOS
fly auth login # opens a browser
cd lifefile/
# 1. Launch the app on Fly.
fly launch --no-deploy --copy-config
# 2. Volume — one-off, holds the SQLite DB and uploaded media.
fly volumes create lifefile_data --region lhr --size 1
# 3. Required secrets.
fly secrets set \
SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(64))')" \
EMAIL_TOKEN_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" \
ANTHROPIC_API_KEY="sk-ant-..."
# 4. Optional secrets (DVLA + DVSA, Google OAuth — see relevant docs).
fly secrets set \
GOOGLE_OAUTH_CLIENT_ID="..." GOOGLE_OAUTH_CLIENT_SECRET="..." \
DVLA_VES_API_KEY="..." \
DVSA_MOT_CLIENT_ID="..." DVSA_MOT_CLIENT_SECRET="..." \
DVSA_MOT_TENANT_ID="..." DVSA_MOT_API_KEY="..." \
VEHICLE_LOOKUP_MODE="live"
# 5. First deploy.
fly deploy
ALLOWED_HOSTS / custom domain¶
ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS live in fly.toml [env]. If you change the app name or add a custom domain, edit them there and fly deploy.
Day-two ops¶
fly logs -a lifefile # tail
fly ssh console -a lifefile # shell into the running VM
fly ssh console -a lifefile -C 'python manage.py shell' # Django shell
fly ssh console -a lifefile -C 'python manage.py createsuperuser'
fly status -a lifefile # machine state, recent deploys
fly secrets list -a lifefile # env-var names (values not shown)
Wipe a user's triage state — useful after pipeline / classifier prompt changes:
fly ssh console -a lifefile -C 'python manage.py wipe_findings --user <username> --dry-run'
fly ssh console -a lifefile -C 'python manage.py wipe_findings --user <username>'
# Add --include-connection to also force a re-OAuth.
fly ssh console -a lifefile -C 'python manage.py wipe_findings --user <username> --include-connection'
Rollback¶
fly releases -a lifefile # list past releases
fly deploy --image registry.fly.io/lifefile:deployment-<id>
…or just revert the offending PR on GitHub and let the next CI deploy roll forward.
Costs¶
- VM: shared-cpu-1x, 512 MB. Auto-stops when idle, auto-starts on request. Free tier covers the development load.
- Volume: 1 GB. ~$0.15/month.
- Egress: free up to 100 GB/month.
Nothing's costing real money today.