Skip to content

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:

fly auth whoami

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:

  1. Runs the test suite.
  2. Builds the Docker image (multi-stage; Pillow + zlib at build time, smaller libjpeg at runtime).
  3. 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:

fly ssh console -a lifefile -C 'python manage.py migrate'

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.