tl/DEPLOY.md
Your Name e6d150c375 feat: production deploy (Fly + R2) + frame controls and picker glow
Deploy:
- Multi-stage Dockerfile (shadow-cljs release → Python+ffmpeg runtime), fly.toml
  (one machine, scale-to-zero, SQLite on a volume), .dockerignore, DEPLOY.md
- Same-origin release build: api-port defaults "9001" for dev, "" in release
  (no CORS in prod)
- env-driven settings (SECRET_KEY/DEBUG/ALLOWED_HOSTS/DB_PATH/CSRF), WhiteNoise
  serving static + the SPA, R2 (S3-compatible) media storage when R2_* is set
- thumbnails reworked storage-agnostic: download clip, ffmpeg select-pass, upload
  tiles via default_storage (R2 in prod, disk in dev)
- pin channels/daphne; add whitenoise/django-storages/boto3

UI:
- frame step buttons flanking play (disabled at timeline bounds)
- Add-annotation button moved into the toolbar, made prominent
- picker mode pulses a 1-bit glow on clips / frame readout / (while linking)
  annotation bars; linking an annotation bar inserts a timeline link

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:56:09 -04:00

2.6 KiB

Deploy (Fly.io + Cloudflare R2)

One Fly machine (scale-to-zero) runs Django ASGI (daphne) + ffmpeg, serving the API, the WebSocket, and the compiled SPA. SQLite lives on a Fly volume. All media — clips, OTIO, thumbnails — lives in Cloudflare R2 (S3-compatible, free egress), served through Cloudflare's CDN. The frontend is a same-origin release build, so there's no CORS in prod.

1. Cloudflare R2 (do this in the dashboard)

  1. Create a bucket (R2 → Create bucket), e.g. lumet-media.
  2. Public access via a custom domain: bucket → Settings → Public access → Connect a domain, e.g. media.yourdomain.com (Cloudflare proxied). That host is R2_PUBLIC_DOMAIN. (This is what makes egress free + cached.)
  3. CORS policy on the bucket (Settings → CORS) — the app fetch()es the OTIO JSON cross-origin, so allow your app origin:
    [{"AllowedOrigins":["https://<your-app>.fly.dev"],
      "AllowedMethods":["GET"],"AllowedHeaders":["*"],"MaxAgeSeconds":3600}]
    
  4. API token: R2 → Manage API Tokens → Create (Object Read & Write, this bucket). Note the Access Key ID, Secret Access Key, and your account's S3 endpoint https://<accountid>.r2.cloudflarestorage.com.

2. Fly secrets

fly secrets set \
  DJANGO_SECRET_KEY="$(python -c 'import secrets;print(secrets.token_urlsafe(64))')" \
  DJANGO_CSRF_TRUSTED="https://<your-app>.fly.dev" \
  R2_BUCKET="lumet-media" \
  R2_ENDPOINT="https://<accountid>.r2.cloudflarestorage.com" \
  R2_ACCESS_KEY_ID="..." \
  R2_SECRET_ACCESS_KEY="..." \
  R2_PUBLIC_DOMAIN="media.yourdomain.com"

3. Deploy

fly volumes create data -r iad -n 1 -s 1   # 1 GB for SQLite (once)
fly deploy
fly ssh console -C "python manage.py createsuperuser"   # first admin

fly.toml already sets DJANGO_DEBUG=0, DJANGO_ALLOWED_HOSTS=.fly.dev, DJANGO_DB_PATH=/data/db.sqlite3, and scale-to-zero.

Notes / caveats

  • Thumbnails run in a background thread. With scale-to-zero the machine can stop right after an upload returns and kill that thread before it finishes — so thumbnails may not generate on the very first idle deploy. Options: bump min_machines_running = 1, or move thumbnailing to a proper queue/worker later. Core app (video, annotations, sync) is unaffected.
  • Single machine only while on SQLite + the in-memory Channels layer. To scale out: LiteFS (SQLite replication) + channels_redis (Upstash).
  • Dev is unchanged: npm run dev still serves UI on :8280 / API on :9001 with local-disk media. R2 only kicks in when the R2_* env vars are set.