Skip to main content

Deployment

This guide covers deploying StreamGate from local development through production, including Docker Compose, deployment topologies, database migration, and scaling considerations.

Local Development Setup

# 1. Install dependencies (npm workspaces)
npm install

# 2. Set up Platform App
cd platform
cp .env.example .env # Configure environment variables
npx prisma migrate dev # Initialize SQLite database
npm run dev # Next.js dev server on :3000

# 3. Set up HLS Server (new terminal)
cd hls-server
npm run dev # Express dev server on :4000

Minimum .env for local development:

platform/.env
DATABASE_URL="file:./dev.db"
PLAYBACK_SIGNING_SECRET="dev-secret-change-me-in-production-32chars"
INTERNAL_API_KEY="dev-internal-api-key-change-me"
ADMIN_PASSWORD_HASH="$2b$12$LJ3m4ys3Lk0TSwMBQWJJF.FzHqKn5A2n3MpGkbP0U7Q67rJFEyxGq"
HLS_SERVER_BASE_URL="http://localhost:4000"
NEXT_PUBLIC_APP_NAME="StreamGate"
SESSION_TIMEOUT_SECONDS="60"

Docker Compose

The included docker-compose.yml runs both services together:

docker-compose.yml
services:
platform:
build:
context: .
dockerfile: platform/Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: "file:./dev.db"
PLAYBACK_SIGNING_SECRET: "dev-secret-change-me-in-production-32chars"
INTERNAL_API_KEY: "dev-internal-api-key-change-me"
ADMIN_PASSWORD_HASH: "$2b$12$..."
HLS_SERVER_BASE_URL: "http://localhost:4000"
NEXT_PUBLIC_APP_NAME: "StreamGate"
SESSION_TIMEOUT_SECONDS: "60"
volumes:
- platform-data:/app/platform/prisma

hls-server:
build:
context: .
dockerfile: hls-server/Dockerfile
ports:
- "4000:4000"
environment:
PLAYBACK_SIGNING_SECRET: "dev-secret-change-me-in-production-32chars"
INTERNAL_API_KEY: "dev-internal-api-key-change-me"
PLATFORM_APP_URL: "http://platform:3000"
STREAM_ROOT: "/streams"
CORS_ALLOWED_ORIGIN: "http://localhost:3000"
PORT: "4000"
volumes:
- ./streams:/streams

volumes:
platform-data:
# Build and start
docker compose up --build

# Run in background
docker compose up -d

# View logs
docker compose logs -f

# Stop
docker compose down
info

In Docker Compose, the HLS server uses http://platform:3000 as the PLATFORM_APP_URL (Docker's internal DNS resolution), while CORS_ALLOWED_ORIGIN uses http://localhost:3000 (the browser's perspective).

Deployment Topologies

Topology A: Co-located (Single Server)

Best for: Development, demos, small-scale events (under 500 viewers).

┌──────────────────────────────────┐
│ Single Server │
│ │
│ ┌───────────┐ ┌─────────────┐ │
│ │ Platform │ │ HLS Server │ │
│ │ App :3000 │ │ :4000 │ │
│ └─────┬─────┘ └──────┬──────┘ │
│ │ │ │
│ ┌─────▼───────────────▼──────┐ │
│ │ Reverse Proxy (nginx) │ │
│ │ :443 (HTTPS) │ │
│ └────────────────────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │ SQLite │ │ /streams │ │
│ │ Database │ │ (files) │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────┘

Configuration:

  • HLS_SERVER_BASE_URL=http://localhost:4000
  • PLATFORM_APP_URL=http://localhost:3000
  • Use nginx to terminate TLS and proxy to both services

Topology B: Separated (PaaS + VPS)

Best for: Production deployments, moderate scale (500–5,000 viewers).

┌─────────────────────┐ ┌────────────────────────┐
│ PaaS (Vercel, etc.) │ │ VPS / Cloud VM │
│ │ │ │
│ ┌────────────────┐ │ │ ┌─────────────────┐ │
│ │ Platform App │ │ poll │ │ HLS Server │ │
│ │ (Next.js) │◄─┼─────────┼──│ (Express) │ │
│ └────────┬───────┘ │ /api/ │ └────────┬────────┘ │
│ │ │ revoc. │ │ │
│ ┌────────▼───────┐ │ │ ┌────────▼────────┐ │
│ │ PostgreSQL │ │ │ │ /streams │ │
│ │ (managed) │ │ │ │ (local files) │ │
│ └────────────────┘ │ │ └─────────────────┘ │
└─────────────────────┘ └────────────────────────┘

Configuration:

  • Platform: HLS_SERVER_BASE_URL=https://hls.example.com
  • HLS: PLATFORM_APP_URL=https://app.example.com
  • HLS: CORS_ALLOWED_ORIGIN=https://app.example.com
  • PostgreSQL via managed database service

Topology C: Edge / Multi-Region

Best for: Global audiences, large scale (5,000+ viewers across regions).

┌─────────────────────┐
│ Central Platform │
│ App (Primary) │
│ + PostgreSQL │
└──────────┬──────────┘

┌────────────────┼────────────────┐
│ │ │
┌─────────▼──────┐ ┌──────▼────────┐ ┌─────▼────────┐
│ HLS Server │ │ HLS Server │ │ HLS Server │
│ US-East │ │ EU-West │ │ AP-Southeast │
│ + Local cache │ │ + Local cache │ │ + Local cache│
└────────────────┘ └───────────────┘ └──────────────┘

Configuration:

  • Each HLS instance polls the central Platform App
  • Use proxy mode with shared upstream origin, or replicate content
  • DNS-based geographic routing (CloudFlare, Route 53)
  • Each HLS instance maintains its own revocation cache
tip

The HLS server's stateless design (no database, JWT-only auth) makes it ideal for edge deployment. Spin up instances close to your viewers for minimal latency.

Database Migration: SQLite → PostgreSQL

Step-by-step

  1. Update the Prisma datasource provider:
platform/prisma/schema.prisma
datasource db {
provider = "postgresql" // Change from "sqlite"
}
  1. Set the production database URL:
DATABASE_URL="postgresql://user:password@host:5432/streamgate?sslmode=require"
  1. Generate new migrations:
cd platform

# If starting fresh (no existing data to preserve):
npx prisma migrate dev --name init-postgres

# If migrating existing data, use Prisma's migration tools or
# export/import data manually
  1. Deploy migrations:
npx prisma migrate deploy
warning

SQLite and PostgreSQL have subtle differences (e.g., case sensitivity, date handling). Test thoroughly after switching providers. The Prisma schema as-is is compatible with both providers.

Scaling Considerations

Platform App

  • Horizontal scaling: Deploy multiple instances behind a load balancer
  • Database: PostgreSQL connection pooling (e.g., PgBouncer) for many instances
  • Rate limiters: In-memory rate limiters are per-instance; use Redis for shared state in multi-instance deployments
  • Sessions: iron-session cookies are self-contained (encrypted in cookie) — no sticky sessions needed

HLS Media Server

MetricApproximate Capacity
JWT verifications~50,000/sec/core
Concurrent viewers per instance~5,000 (depends on bitrate and network)
Revocation cache memory~100 bytes per entry
  • CPU-bound: JWT verification is the primary CPU cost
  • I/O-bound: Segment serving is I/O bound (disk or network)
  • Stateless: No shared state between instances (each has own revocation cache)
  • Scale-out: Add more instances behind a load balancer

Content Delivery

  • Local mode: Disk I/O is the bottleneck; use SSDs
  • Proxy mode: Network to upstream is the bottleneck; segment cache reduces repeated fetches
  • Hybrid mode: Best of both — local for known content, upstream for dynamic content

Environment Variable Reference

Shared Variables

These must match between Platform App and HLS Server:

VariableRequiredDescription
PLAYBACK_SIGNING_SECRETYesHMAC-SHA256 secret for JWT signing/verification. Minimum 32 characters. Must be identical on both services.
INTERNAL_API_KEYYesAPI key for revocation sync endpoint. Must be identical on both services.

Platform App Variables

VariableRequiredDefaultDescription
DATABASE_URLYesPrisma database connection string
ADMIN_PASSWORD_HASHYesbcrypt hash of admin password
HLS_SERVER_BASE_URLYesBase URL of HLS server (e.g., http://localhost:4000)
NEXT_PUBLIC_APP_NAMENo"StreamGate"Application name shown in UI
SESSION_TIMEOUT_SECONDSNo60Seconds before inactive session is considered abandoned
ADMIN_PASSWORD_HASH_FILENoAlternative: read admin hash from a file (for Docker secrets)

HLS Server Variables

VariableRequiredDefaultDescription
PLATFORM_APP_URLYesBase URL of Platform App for revocation polling
CORS_ALLOWED_ORIGINYesAllowed CORS origin (Platform App URL as seen by browser)
PORTNo4000HTTP port to listen on
STREAM_ROOTConditionalRoot directory for local stream files. At least one of STREAM_ROOT or UPSTREAM_ORIGIN required.
UPSTREAM_ORIGINConditionalUpstream origin URL for proxy mode. At least one of STREAM_ROOT or UPSTREAM_ORIGIN required.
SEGMENT_CACHE_ROOTNoSTREAM_ROOT/cache/Root directory for cached upstream segments
SEGMENT_CACHE_MAX_SIZE_GBNo50Maximum segment cache size in GB (LRU eviction)
SEGMENT_CACHE_MAX_AGE_HOURSNo72Maximum age of cached segments in hours
REVOCATION_POLL_INTERVAL_MSNo30000Revocation polling interval in milliseconds

Generating Secrets

Signing Secret (PLAYBACK_SIGNING_SECRET)

# Method 1: OpenSSL
openssl rand -base64 32

# Method 2: Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

# Method 3: Python
python -c "import secrets; print(secrets.token_urlsafe(32))"

Internal API Key (INTERNAL_API_KEY)

# Use same generation methods as above
openssl rand -hex 32

Admin Password Hash (ADMIN_PASSWORD_HASH)

# Node.js with bcrypt
node -e "const bcrypt = require('bcrypt'); bcrypt.hash('your-secure-password', 12).then(console.log)"

# Or use htpasswd
# Note: bcrypt hashes contain $ characters — use ADMIN_PASSWORD_HASH_FILE
# or read directly from .env to avoid shell expansion issues
danger
  • Never reuse secrets across environments (dev/staging/prod)
  • Never commit secrets to version control
  • Use Docker secrets or a secrets manager in production
  • The default docker-compose.yml values are for development only