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>
2.6 KiB
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)
- Create a bucket (R2 → Create bucket), e.g.
lumet-media. - Public access via a custom domain: bucket → Settings → Public access →
Connect a domain, e.g.
media.yourdomain.com(Cloudflare proxied). That host isR2_PUBLIC_DOMAIN. (This is what makes egress free + cached.) - 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}] - 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 devstill serves UI on :8280 / API on :9001 with local-disk media. R2 only kicks in when theR2_*env vars are set.