Deployment
Local Development (Docker Compose)
The fastest way to run the full stack locally. Spins up the API, worker, transcoder, dashboard, PostgreSQL, Redis, and MinIO in one command.
git clone https://github.com/Synapsr/strum-vod.git
cd strum-vod
cp .env.example .env
docker compose up -d --build| Service | Host Port | Description |
|---|---|---|
| API | 13002 | REST API |
| Dashboard | 13003 | Management UI |
| PostgreSQL | 15432 | Database |
| Redis | 16379 | Job queue |
| MinIO | 19000 | S3-compatible storage |
| MinIO Console | 19001 | Storage admin UI |
docker:infrastarts only the infrastructure services (postgres,redis,minio,minio-init);docker:devruns infra + the fullpnpm devstack.
Stopping
docker compose down # stop services, keep data
docker compose down -v # stop services + delete volumesDevelopment Without Docker
Requires local PostgreSQL, Redis, FFmpeg, Go, and MinIO (or any S3-compatible store).
Prerequisites
- Node.js >= 20
- pnpm >= 9
- Go 1.23+ (for
apps/transcoder) - PostgreSQL 16+
- Redis 7.x
- FFmpeg + ffprobe (transcoder)
- yt-dlp (URL imports)
Setup
git clone https://github.com/Synapsr/strum-vod.git
cd strum-vod
pnpm install
cp .env.example .envEdit .env for local services:
DATABASE_URL=postgresql://strum_vod:strum-vodpassword@localhost:5432/strum_vod
REDIS_URL=redis://localhost:6379
S3_ENDPOINT=http://localhost:19000
S3_PUBLIC_ENDPOINT=http://localhost:19000
S3_PUBLIC_BASE_URL=http://localhost:19000/strum-vod
VITE_API_BASE_URL=http://localhost:13002Start Each Service
pnpm run build -w @strum-vod/db # must build first
pnpm run dev -w @strum-vod/api # API
pnpm run dev -w @strum-vod/worker # Worker (Node: bridge + AI)
pnpm run dev:transcoder # Transcoder (Go: ffmpeg ladder)
pnpm run dev -w @strum-vod/dashboard # Dashboard on :1337
pnpm run dev:player # Player app on :1338Production: Split Deployment
In production, each app runs on the platform that suits it best.
| App | Platform | Command |
|---|---|---|
apps/api | Fly.io (or Railway, any VPS) | fly deploy --config apps/api/fly.toml |
apps/worker | Fly.io | fly deploy --config fly.worker.toml |
apps/transcoder | Fly.io / dedicated server | fly deploy --config fly.transcoder.toml |
apps/dashboard | Cloudflare Workers + Assets | wrangler deploy --config apps/dashboard/wrangler.toml |
apps/player | Cloudflare Pages | pnpm deploy:player (wrangler pages) |
Resumable (TUS) video upload has no separate deploy target — it's part of apps/api (src/routes/tus.ts), so it ships with the API.
1. Database & Redis
Use a managed PostgreSQL 16+ instance (Neon, Supabase, AWS RDS) and a managed Redis (Upstash, Redis Cloud):
DATABASE_URL=postgresql://user:password@ep-xyz.region.aws.neon.tech/neondb?sslmode=require
REDIS_URL=redis://default:password@my-redis.upstash.io:6379Neon: use the direct endpoint (drop
-pooler) — seedocs/configuration.mdandDEPLOYMENT-juninho.md.
2. Storage — Cloudflare R2
Create R2 buckets and enable public access:
wrangler r2 bucket create strum-vod
wrangler r2 bucket create strum-vod-backupsApply CORS rules from the repo (required for browser-direct presigned uploads):
pnpm r2:cors:setR2 does not support per-object
ACL: public-readorPutBucketCorsvia the S3 API. Bucket-level CORS is applied with wrangler (seeinfra/r2/strum-vod-cors.json).
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_REGION=auto
S3_BUCKET=strum-vod
S3_ACCESS_KEY_ID=<R2 key id>
S3_SECRET_ACCESS_KEY=<R2 secret>
S3_FORCE_PATH_STYLE=false
S3_PUBLIC_BASE_URL=https://pub-<hash>.r2.dev3. API, Worker & Transcoder (Fly.io)
# Authenticate
fly auth login
# Create apps (first time)
fly apps create strum-vod-api --machines
fly apps create strum-vod-worker --machines
fly apps create strum-vod-transcoder --machines
# Set secrets on each app (repeat per app)
fly secrets set \
DATABASE_URL="postgresql://..." \
REDIS_URL="redis://..." \
S3_ENDPOINT="https://..." \
S3_ACCESS_KEY_ID="..." \
S3_SECRET_ACCESS_KEY="..." \
S3_PUBLIC_BASE_URL="https://..." \
JWT_SECRET="$(openssl rand -hex 32)" \
SHARED_AUTH_SECRET="$(openssl rand -base64 32)" \
--app strum-vod-api
# Deploy
fly deploy --config apps/api/fly.toml
fly deploy --config fly.worker.toml
fly deploy --config fly.transcoder.tomlRuntime models:
- API (
apps/api/fly.toml) — always-on. - Worker (
fly.worker.toml) — autostart/autostop. The API wakes it via HTTP/wake(or the Fly Machines API,FLY_API_TOKEN) whenever a transcode job is enqueued. - Transcoder (
fly.transcoder.toml) — runs continuously (min_machines_running=1); it's woken by Redis Stream activity, not HTTP, so sleep/wake isn't worth it for the low-idle Go binary.
4. Dashboard (Cloudflare Workers + Assets)
# Build with production env vars
VITE_API_BASE_URL=https://api.strum-vod.fly.dev \
VITE_TUS_SERVER_URL=https://api.strum-vod.fly.dev \
VITE_PLAYER_BASE_URL=https://player.strum-vod.dev \
pnpm run build -w @strum-vod/dashboard
# Deploy
wrangler deploy --config apps/dashboard/wrangler.toml5. Player App (Cloudflare Pages)
# Create apps/player/.env with:
# VITE_API_BASE_URL=https://api.strum-vod.fly.dev
pnpm run deploy:player # vite build + wrangler pages deploy --project-name=strum-vod-player6. Docs Site (VitePress, Cloudflare Pages)
The documentation site (docs/, built with VitePress) deploys to Cloudflare Pages automatically via CI — no manual step needed.
One-time setup:
# Create the Pages project (first time only)
wrangler pages project create strum-vod-docs --production-branch mainThen set the custom domain (e.g. docs.strum-vod.dev) in the Cloudflare dashboard → Pages → strum-vod-docs → Custom domains.
Required GitHub Actions secrets:
| Secret | Value |
|---|---|
CLOUDFLARE_API_TOKEN | API token with Account → Cloudflare Pages → Edit permission |
CLOUDFLARE_ACCOUNT_ID | Cloudflare account ID (dashboard → right sidebar) |
Trigger: .github/workflows/docs.yml rebuilds and redeploys on every push to main touching docs/** or the root markdown the site imports (README.md, DOCKER.md, CONTRIBUTING.md, SECURITY.md, CHANGELOG.md, CLAUDE.md, package.json, pnpm-lock.yaml). Pull requests get a unique preview URL; workflow_dispatch forces a redeploy.
Manual deploy (local):
pnpm run deploy:docs # pnpm docs:build + wrangler pages deployOnce deployed, point the dashboard sidebar's "Documentation" link at it with
VITE_DOCS_URL=https://docs.strum-vod.devat dashboard build time.
Reverse Proxy (nginx / Caddy)
For the all-in-one Docker image or self-hosted VPS deployment:
api.yourdomain.com → api (:3000)
dashboard.yourdomain.com → (Cloudflare Workers + Assets)
player.yourdomain.com → (Cloudflare Pages)Caddy example
api.yourdomain.com {
reverse_proxy localhost:3000
}Scaling the Transcoder
The Go transcoder is stateless. Run multiple instances to process videos in parallel — each picks jobs from the same Redis Stream consumer group (XREADGROUP, with XCLAIM-based recovery of orphaned messages):
# Fly.io
fly scale count 3 --app strum-vod-transcoderEach instance auto-detects its own CPU/RAM (cgroup-aware) and adjusts its FFmpeg concurrency. The Node worker (bridge + AI) scales the same way.
One-Click Deploy
For the all-in-one Docker image:
| Platform | How to deploy |
|---|---|
| EasyPanel | Add Docker app → synapsr/strum-vod |
| Dokploy | Import from Docker Hub |
| Coolify | One-click from Docker image |
| Portainer | Create stack from compose |
| Railway | Deploy from Docker image |