Docker Deployment Guide
Strum VOD provides multiple deployment modes, from a single docker run command to a fully split production architecture. This guide covers every option with architecture diagrams, configuration, and scaling advice.
Table of Contents
- Architecture Overview
- Deployment Modes
- Dockerfiles Reference
- Environment Variables
- Scaling
- Volumes & Data Persistence
- Building from Source
- Networking & Ports
Architecture Overview
Strum VOD is composed of 5 logical services that connect to 3 infrastructure backends:
┌─────────────────────────────────────────────┐
│ Strum VOD Platform │
│ │
Browser ───────────────>│ ┌───────────┐ ┌───────────────┐ │
│ │ Dashboard │────────>│ API Server │ │
│ │ (React) │ │ (Fastify) │ │
│ └───────────┘ └───────┬───────┘ │
│ │ │
│ BullMQ job │
│ │ │
│ ┌───────▼───────┐ │
│ │ Worker (Node)│ │
│ │ bridge + AI │ │
│ └───────┬───────┘ │
│ Redis Stream │ │
│ ┌───────▼───────┐ │
│ │ Transcoder │ │
│ │ (Go + FFmpeg) │ │
│ └───────┬───────┘ │
└────────────────────────────────┼───────────┘
│
┌──────────────────────────────────────┼────────────────┐
│ Infrastructure │
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────────────┐ │
│ │ Postgres│ │ Redis │ │ S3 Storage │ │
│ │ (state)│ │ (queue) │ │ (videos, HLS) │ │
│ └─────────┘ └─────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────┘Data Flow
1. Upload 2. Transcode 3. Playback
Client ──POST──> API API ──job──> Redis Browser ──GET──> S3
│ │ (direct HLS streaming,
▼ ▼ API not involved)
S3 (source upload) Node worker relays
Postgres (asset record) to Go transcoder
│
▼
FFmpeg HLS ladder
(360p–4320p, single pass)
│
▼
S3 (HLS segments)
Postgres (status → ready)Key design: Video playback is served directly from S3. The API only handles metadata and coordination. This means S3 absorbs all bandwidth, and the API stays lightweight.
Pipeline split: the Node worker (apps/worker) is now just a bridge (BullMQ ↔ Redis Streams) + AI pipeline; the actual FFmpeg ladder lives in the Go transcoder (apps/transcoder).
Deployment Modes
Mode 1: All-in-One (Simplest)
Best for: Getting started, small teams, personal use, VPS/single-server deployments.
Everything runs in a single container. PostgreSQL and Redis are embedded and managed automatically. You only provide S3 credentials.
┌──────────────────────────────────────────────────────┐
│ Strum VOD Container │
│ (port 3000) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ entrypoint.sh (process orchestrator) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │ │
│ │ │ Postgres │ │ Redis │ │ Worker │ │ │
│ │ │ (auto) │ │ (auto) │ │ (Node) │ │ │
│ │ └──────────┘ └──────────┘ └─────────────┘ │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Transcoder (Go) + API + Dashboard │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ /data (volume) │
│ ├── postgres/ PostgreSQL data files │
│ └── redis/ Redis persistence │
└──────────────────────────────────────────────────────┘
│
▼
S3 Storage (external)docker run -d \
--name strum-vod \
-p 3000:3000 \
-v strum-vod-data:/data \
-e S3_ENDPOINT=https://s3.amazonaws.com \
-e S3_REGION=us-east-1 \
-e S3_BUCKET=my-bucket \
-e S3_ACCESS_KEY_ID=AKIA... \
-e S3_SECRET_ACCESS_KEY=... \
-e S3_PUBLIC_BASE_URL=https://my-bucket.s3.amazonaws.com \
-e S3_FORCE_PATH_STYLE=false \
synapsr/strum-vodWhat happens:
entrypoint.shinitializes PostgreSQL (trust auth, localhost only) and creates thestrum_voddatabase- Starts Redis with persistence (
save 60 1) - Starts the Go transcoder and the Node worker in the background
- Starts the API (serves the Dashboard SPA) in the foreground
- Graceful shutdown on
SIGTERM/SIGINTstops all processes
Characteristics:
| Aspect | Detail |
|---|---|
| Image | synapsr/strum-vod (all-in-one) |
| Port | 3000 (API + Dashboard) |
| Volume | /data (PostgreSQL + Redis) |
| PostgreSQL | Embedded, localhost only |
| Redis | Embedded, localhost only, persistence enabled |
| Transcoder | Go binary, hardware-adaptive concurrency |
| Worker | Node bridge + AI pipeline |
| Dashboard | Served by API via @fastify/static (same origin) |
Mode 2: All-in-One + External Database
Best for: Production single-server, when you want managed PostgreSQL (Neon, RDS, etc.) or managed Redis (Upstash, ElastiCache, etc.).
Same image as Mode 1, but the entrypoint skips embedded services when their URL is provided.
docker run -d \
--name strum-vod \
-p 3000:3000 \
-e DATABASE_URL=postgresql://user:pass@neon-host:5432/neondb?sslmode=require \
-e REDIS_URL=redis://elasticache-host:6379 \
-e S3_ENDPOINT=https://s3.amazonaws.com \
-e S3_REGION=us-east-1 \
-e S3_BUCKET=my-bucket \
-e S3_ACCESS_KEY_ID=AKIA... \
-e S3_SECRET_ACCESS_KEY=... \
-e S3_PUBLIC_BASE_URL=https://my-bucket.s3.amazonaws.com \
-e S3_FORCE_PATH_STYLE=false \
synapsr/strum-vodYou can also mix: use external PostgreSQL with embedded Redis, or vice versa. Only set the env vars for services you want external.
# External PostgreSQL, embedded Redis
-e DATABASE_URL=postgresql://user:pass@neon-host:5432/neondb?sslmode=require
# (omit REDIS_URL → embedded Redis starts automatically)Mode 3: Docker Compose — Split Services
Best for: Local development, staging, teams who want to inspect each service independently.
Each service runs in its own container. PostgreSQL, Redis, and MinIO run as separate containers.
┌────────────────────────────── Docker Network ──────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ Postgres │ │ Redis │ │ MinIO │ │ MinIO Init │ │
│ │ 16 │ │ 7 │ │ (S3) │ │ (create bucket) │ │
│ │ :5432 │ │ :6379 │ │ :9000 │ │ (one-shot) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────────────────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ API Server │◄──────── ┌──────────────┐ │
│ │ :3000→13002 │ │ Dashboard │ │
│ └────────┬───────┘ │ :3001→13003 │ │
│ │ └──────────────┘ │
│ BullMQ job │
│ │ │
│ ┌────────▼────────┐ │
│ │ Worker (Node) │ │
│ │ no port │ │
│ └────────┬───────┘ │
│ │ Redis Stream │
│ ┌────────▼────────┐ │
│ │ Transcoder (Go)│ │
│ │ no port │ │
│ └────────────────┘ │
│ │
│ Shared volumes: uploads (API ↔ Worker ↔ Transcoder), postgres-data, │
│ minio-data │
└────────────────────────────────────────────────────────────────────────────┘git clone https://github.com/Synapsr/strum-vod.git && cd strum-vod
cp .env.example .env
docker compose up -d --build| Service | Internal port | External port | Purpose |
|---|---|---|---|
postgres | 5432 | 15432 | Database |
redis | 6379 | 16379 | Job queue |
minio | 9000 / 9001 | 19000 / 19001 | S3 storage / web console |
api | 3000 | 13002 | REST API |
worker | — | — | Node bridge + AI |
transcoder | — | — | Go FFmpeg ladder |
dashboard | 3001 | 13003 | React SPA |
glitchtip* | 8000 | 18000 | Error tracking (profile glitchtip) |
Access points:
- Dashboard: http://localhost:13003
- API: http://localhost:13002
- MinIO Console: http://localhost:19001
Start only the infrastructure (Postgres/Redis/MinIO) with
pnpm docker:infra, then run the app viapnpm devfor hot reload.
Mode 4: Production — Full Split
Best for: High-volume production, Kubernetes, horizontal scaling, when you need multiple workers.
Each Strum VOD service uses its own dedicated Dockerfile. Infrastructure (PostgreSQL, Redis, S3) is managed externally.
Load Balancer / Reverse Proxy
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌─────────────┐
│ Dashboard │ │ API │ │ API │
│ (CF │ │ replica 1 │ │ replica 2 │
│ Workers) │ └─────┬─────┘ └──────┬──────┘
└───────────┘ │ │
└────────┬────────┘
│
┌────────────▼────────────┐
│ Redis (managed) │
│ BullMQ + Redis Streams │
└────────────┬────────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌─────▼───────┐
│ Transcoder1│ │ Transcoder2│ │ Worker 1 │
│ (Go) │ │ (Go) │ │ (Node) │
└──────┬─────┘ └──────┬─────┘ └──────┬──────┘
│ │ │
└───────────────┼───────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Postgres │ │ S3 / CDN │ │ Redis │
│ (managed) │ │ (video │ │ (managed) │
│ (Neon/RDS)│ │ delivery)│ │ │
└───────────┘ └───────────┘ └───────────┘API
docker build -f apps/api/Dockerfile -t strum-vod-api .
docker run -d \
--name strum-vod-api \
-p 3000:3000 \
-e DATABASE_URL=postgresql://user:pass@db-host:5432/strum_vod \
-e REDIS_URL=redis://redis-host:6379 \
-e S3_ENDPOINT=https://s3.amazonaws.com \
-e S3_REGION=us-east-1 \
-e S3_BUCKET=my-bucket \
-e S3_ACCESS_KEY_ID=AKIA... \
-e S3_SECRET_ACCESS_KEY=... \
-e S3_PUBLIC_BASE_URL=https://cdn.example.com \
-e S3_FORCE_PATH_STYLE=false \
-e JWT_SECRET=$(openssl rand -hex 32) \
strum-vod-apiWorker (Node bridge + AI)
docker build -f apps/worker/Dockerfile -t strum-vod-worker .
docker run -d --name strum-vod-worker-1 \
-e DATABASE_URL=postgresql://user:pass@db-host:5432/strum_vod \
-e REDIS_URL=redis://redis-host:6379 \
-e S3_ENDPOINT=https://s3.amazonaws.com \
-e S3_REGION=us-east-1 \
-e S3_BUCKET=my-bucket \
-e S3_ACCESS_KEY_ID=AKIA... \
-e S3_SECRET_ACCESS_KEY=... \
strum-vod-workerTranscoder (Go — scale horizontally)
docker build -f apps/transcoder/Dockerfile -t strum-vod-transcoder .
docker run -d --name strum-vod-transcoder-1 \
-e DATABASE_URL=postgresql://user:pass@db-host:5432/strum_vod \
-e REDIS_URL=redis://redis-host:6379 \
-e S3_ENDPOINT=https://s3.amazonaws.com \
-e S3_REGION=us-east-1 \
-e S3_BUCKET=my-bucket \
-e S3_ACCESS_KEY_ID=AKIA... \
-e S3_SECRET_ACCESS_KEY=... \
strum-vod-transcoderEach transcoder auto-detects its own CPU/RAM and adjusts concurrency. Multiple instances share the same Redis Stream consumer group. See Scaling.
Dashboard
docker build -f apps/dashboard/Dockerfile \
--build-arg VITE_API_BASE_URL=https://api.example.com \
-t strum-vod-dashboard .
docker run -d --name strum-vod-dashboard -p 3001:3001 strum-vod-dashboardTip: In production, deploy the Dashboard build output (
apps/dashboard/dist/) to Cloudflare Workers + Assets viawrangler deploy --config apps/dashboard/wrangler.tomlinstead of running a Node.js container for it.
Dockerfiles Reference
| File | Image | Contains | Port | Size |
|---|---|---|---|---|
Dockerfile | synapsr/strum-vod | API + Worker + Transcoder + Dashboard + PostgreSQL + Redis + FFmpeg | 3000 | ~1 GB |
apps/api/Dockerfile | strum-vod-api | API server only | 3000 | ~200 MB |
apps/worker/Dockerfile | strum-vod-worker | Worker + FFmpeg + pg_dump | — | ~350 MB |
apps/transcoder/Dockerfile | strum-vod-transcoder | Go transcoder + FFmpeg + yt-dlp | — | ~150 MB |
apps/dashboard/Dockerfile | strum-vod-dashboard | React SPA + serve | 3001 | ~150 MB |
All Dockerfiles use pnpm (via corepack) and multi-stage builds (build → runtime) for minimal image sizes. Build artifacts and node_modules are pruned.
Environment Variables
Required (all modes)
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string (postgresql://user:pass@host:5432/db) |
S3_ENDPOINT | S3-compatible endpoint URL |
S3_REGION | S3 region |
S3_BUCKET | S3 bucket name |
S3_ACCESS_KEY_ID | S3 access key |
S3_SECRET_ACCESS_KEY | S3 secret key |
S3_PUBLIC_BASE_URL | Public URL to access S3 objects (for HLS playback) |
Required (split mode only)
| Variable | Description |
|---|---|
REDIS_URL | Redis connection string (redis://host:6379) |
JWT_SECRET | Secret for JWT auth tokens (min 32 chars, openssl rand -hex 32) |
Optional
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | API/dashboard port |
S3_FORCE_PATH_STYLE | false | Path-style S3 URLs (true for MinIO/Backblaze) |
S3_PUBLIC_ENDPOINT | same as S3_ENDPOINT | Public S3 endpoint for browser uploads |
CORS_ORIGIN | * | Allowed CORS origins (comma-separated) |
DASHBOARD_URL | http://localhost:3001 | Base URL for embed player URLs |
SHARED_AUTH_SECRET | — | Enables TUS resumable upload (openssl rand -base64 32) |
Scaling (auto-detected, override via env)
| Variable | Default | Description |
|---|---|---|
WORKER_CONCURRENCY | auto | Concurrent transcode jobs per transcoder |
FFMPEG_THREADS | auto | Threads per FFmpeg process |
DB_POOL_SIZE | auto | PostgreSQL connection pool size |
The transcoder logs its computed config at startup:
[transcoder] Hardware-adaptive config:
CPU cores: 8
Total RAM: 16.0 GB
Concurrency: 2 job(s)
FFmpeg threads: 4 per job
DB pool size: 6See the Scaling section below for formulas and recommendations.
Scaling
How auto-detection works
At startup, the Go transcoder reads CPU core count and total RAM (cgroup-aware) to compute:
Concurrency = max(1, min( floor((RAM - 1GB) / 1.5GB), floor(cores / 4) ))
FFmpeg threads = max(1, floor(cores / concurrency))
DB pool = max(5, concurrency * 2 + 2)| Machine | Concurrency | FFmpeg threads | DB pool |
|---|---|---|---|
| 2 cores, 4 GB | 1 job | 2 | 5 |
| 4 cores, 8 GB | 1 job | 4 | 5 |
| 8 cores, 16 GB | 2 jobs | 4 | 6 |
| 16 cores, 32 GB | 4 jobs | 4 | 10 |
| 32 cores, 64 GB | 8 jobs | 4 | 18 |
Horizontal scaling (multiple transcoders)
Transcoders are stateless. Run multiple instances against the same Redis Stream consumer group to increase throughput:
┌────────────────────┐
│ Redis (Streams) │
│ shared queue │
└─────────┬──────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌─────▼───────┐ ┌─────▼───────┐
│ Transcoder 1│ │ Transcoder 2│ │ Transcoder 3│
│ 8 cores │ │ 4 cores │ │ 16 cores │
│ 2 jobs │ │ 1 job │ │ 4 jobs │
└────────────┘ └─────────────┘ └─────────────┘
│
Total throughput: 7 concurrent transcodesEach transcoder auto-detects its own hardware independently. Heterogeneous machines work fine.
# Scale transcoders in Docker Compose
docker compose up -d --scale transcoder=3
# Or run separate containers
docker run -d --name transcoder-1 -e ... strum-vod-transcoder
docker run -d --name transcoder-2 -e ... strum-vod-transcoder
docker run -d --name transcoder-3 -e ... strum-vod-transcoderAPI scaling
The API is stateless (all state lives in Postgres/Redis). Run multiple replicas behind a load balancer:
docker compose up -d --scale api=2Note: The Dashboard in split mode is a static SPA. It can be served from Cloudflare Workers + Assets without a Node.js runtime.
Volumes & Data Persistence
All-in-One mode
| Path | Content | Critical |
|---|---|---|
/data/postgres/ | PostgreSQL data files | Yes — losing this loses all metadata |
/data/redis/ | Redis RDB/AOF snapshots | Low — only job queue state |
docker run -v strum-vod-data:/data ...Split mode (Docker Compose)
| Volume | Used by | Content |
|---|---|---|
postgres-data | PostgreSQL | Database files |
minio-data | MinIO | Video files + HLS output |
uploads | API + Worker + Transcoder | Temporary upload buffer (shared) |
The
uploadsvolume is only needed when using the direct upload endpoint (PUT /v1/assets/:id/upload). If you use pre-signed S3 URLs for uploads (POST /v1/assets/:id/upload-url), the volume can be omitted.
Building from Source
git clone https://github.com/Synapsr/strum-vod.git && cd strum-vod
# All-in-one image
docker build -t strum-vod .
# Individual images
docker build -f apps/api/Dockerfile -t strum-vod-api .
docker build -f apps/worker/Dockerfile -t strum-vod-worker .
docker build -f apps/transcoder/Dockerfile -t strum-vod-transcoder .
docker build -f apps/dashboard/Dockerfile \
--build-arg VITE_API_BASE_URL=https://api.example.com \
-t strum-vod-dashboard .Build order: The Dockerfiles handle the build order internally (@strum-vod/db is built first). No manual steps required.
Networking & Ports
All-in-One
| Port | Service |
|---|---|
| 3000 | API + Dashboard (single port) |
PostgreSQL and Redis bind to 127.0.0.1 (localhost only, not exposed).
Docker Compose (default)
| External port | Internal port | Service |
|---|---|---|
| 13002 | 3000 | API |
| 13003 | 3001 | Dashboard |
| 15432 | 5432 | PostgreSQL |
| 16379 | 6379 | Redis |
| 19000 | 9000 | MinIO S3 API |
| 19001 | 9001 | MinIO Console |
Internal communication
Dashboard ──HTTP──> API (:3000)
API ──postgres-js──> PostgreSQL (:5432)
API ──ioredis──> Redis (:6379)
API ──BullMQ──> Redis (:6379) ──> Worker (bridge)
Worker ──XADD──> go:transcode:jobs ──> Transcoder (Go)
Transcoder ──AWS SDK──> S3 (:9000)
Transcoder ──XADD──> go:ai:dispatch ──> Worker (AI pipeline)
Browser ──HLS──> S3 (direct, public URLs)Quick Reference
| I want to... | Use |
|---|---|
| Try Strum VOD in 30 seconds | Mode 1: All-in-One |
| Run in production on a VPS | Mode 2: All-in-One + External DB |
| Develop locally | Mode 3: Docker Compose |
| Scale for high volume | Mode 4: Full Split |
| Add more transcode capacity | Scaling: multiple transcoders |