Deploy
StreamHub supports two deploy shapes on top of the same single-node architecture. Both need a Linux host with a public IP — LiveKit uses host networking for UDP media and STUN external-IP detection, so Docker Desktop on macOS is not a target.
- A. Docker Compose + Caddy — the OSS quick-install path. One command, automatic TLS.
- B. systemd + nginx + certbot — the plain-server path (used in production on
stream01).
What gets built
Section titled “What gets built”The deployable unit is the NestJS core (which also serves the compiled React web dashboard as static assets), the LiveKit stack (server + ingress + egress + redis), and the browser SDK:
# streamhub-core (the brain + SPA host)cd streamhub-corenpm cinpm run build # → dist/ (nest build); runtime entry: node dist/main.js
# streamhub-web (React SPA) — its dist/ is served by core as static assetscd ../streamhub-webnpm ci && npm run build # → dist/ (Vite + Tailwind)
# streamhub-adaptor (browser SDK) — the drop-in AntMedia WebRTCAdaptor shimcd ../streamhub-adaptornpm ci && npm run build # → dist/streamhub-adaptor.global.jsWith Docker Compose these builds happen inside the image (deploy/Dockerfile) — you never run
them by hand.
Choose a deploy shape
Section titled “Choose a deploy shape”One-liner installer
Section titled “One-liner installer”curl -fsSL https://www.streamhub.studio/install.sh | sudo bashinstall.sh is idempotent and:
- checks prerequisites (docker + the compose plugin, openssl, curl);
- clones the repo if run standalone (into
./vision-media-server); - prompts for
STREAMHUB_DOMAIN,ADMIN_PASS,ACME_EMAIL— or reuses an existing.env. Pre-set any ofSTREAMHUB_DOMAIN/ADMIN_USER/ADMIN_PASS/ACME_EMAILin the environment to run it non-interactively; - generates a
.envwith strong random secrets:LIVEKIT_API_KEY/LIVEKIT_API_SECRET,STREAMHUB_JWT_SECRET,ADMIN_PASS,STREAMHUB_API_TOKEN(sk_…); - builds and starts the stack:
docker compose up -d --build; - waits for core health, then seeds the global API token into the DB
(
deploy/seed-token.js).
Manual Compose
Section titled “Manual Compose”cp .env.example .env # then edit — see the ENV referencedocker compose up -d --builddocker compose ps # redis, livekit, ingress, egress, core, caddy → healthyCompose services (docker-compose.yml): redis (7-alpine), livekit
(livekit/livekit-server:v1.8.4), ingress, egress, core (streamhub-core:local, built
from deploy/Dockerfile), and caddy (2-alpine, deploy/Caddyfile, automatic TLS). Caddy
routes /rtc to LiveKit and everything else to core, on one TLS vhost.
Updating
Section titled “Updating”Re-running the installer is the supported update path — it’s idempotent and reuses the
existing .env:
curl -fsSL https://www.streamhub.studio/install.sh | sudo bashOr update manually from a checked-out repo:
git pulldocker compose up -d --builddocker compose logs -f core # watch it come back up.env changes are only fully picked up on down + up -d (a recreate), not on restart.
Used in production on stream01 (OVH, Ploi-managed, Ubuntu). LiveKit and core run as
systemd units, ingress/egress as Docker containers, redis native, TLS via
nginx + certbot.
1. Base + LiveKit
apt-get install -y redis-server ffmpeg nginx certbot python3-certbot-nginxcurl -sSL https://get.livekit.io | bash # livekit-servercurl -sSL https://get.livekit.io/cli | bash # lk CLIlivekit-server generate-keys # → API_KEY / API_SECRET# deploy/livekit.yaml → /etc/livekit/livekit.yaml (chmod 600)# deploy/livekit.service → /etc/systemd/system/livekit.servicesystemctl enable --now livekit2. ingress + egress (Docker, host networking)
curl -fsSL https://get.docker.com | shdocker run -d --name ingress --restart unless-stopped --network host \ -e INGRESS_CONFIG_BODY="$(cat deploy/ingress.yaml)" livekit/ingress:latestdocker run -d --name egress --restart unless-stopped --network host --shm-size=1g \ -e EGRESS_CONFIG_BODY="$(cat deploy/egress.yaml)" \ -v "$DATA_DIR:/data" livekit/egress:latest3. streamhub-core
cd streamhub-core && npm ci && npm run build# deploy/streamhub-core.service → /etc/systemd/system/ (ExecStart: node dist/main.js, bind 127.0.0.1:3020)# env in the unit / EnvironmentFile — see the ENV referencesystemctl enable --now streamhub-core4. nginx + TLS
cp deploy/nginx-streamhub.conf /etc/nginx/sites-available/streamhub.example.comln -s /etc/nginx/sites-available/streamhub.example.com /etc/nginx/sites-enabled/rm -f /etc/nginx/sites-enabled/defaultnginx -t && systemctl reload nginxcertbot --nginx -d streamhub.example.com --redirect -m "$ACME_EMAIL" --agree-tos -nThe vhost proxies /api/, /hls/, /sdk/, /samples/ and / to 127.0.0.1:3020, and
/rtc (WebSocket upgrade) to 127.0.0.1:7880. /metrics is denied from the outside
(location = /metrics { deny all; return 403; } in deploy/nginx-streamhub.conf, which
install.sh copies verbatim) — Prometheus scrapes the core locally at
127.0.0.1:3020/metrics.
5. Seed the global API token (idempotent):
node deploy/seed-token.js6. Cert renewal — keep certbot.timer active, plus a backup cron entry:
0 3 * * * certbot renew --quiet && systemctl reload nginxUpdating — deploy/deploy-core.sh
Section titled “Updating — deploy/deploy-core.sh”The old deploy-streamhub.sh baked secrets into the script (it’s gitignored — never commit
it). Its replacement, deploy/deploy-core.sh, never takes or writes secrets: it reuses the
.env that already lives in APP_DIR, backs up first, then ships new code and restarts.
# On the build side: tar the core source at the archive roottar czf /tmp/core.tgz -C streamhub-core .scp /tmp/core.tgz deploy@stream01:/tmp/
# On the host:APP_DIR=/opt/skyline-core SERVICE_NAME=skyline \ deploy/deploy-core.sh /tmp/core.tgzIt, in order:
- checks
APP_DIR/.envexists (aborts rather than invent secrets); - runs
deploy/backup.sh(aborts the deploy if the backup fails — override with--skip-backup); tar-overlays the new code ontoAPP_DIR(never touching.env,data/,apps/,logs/— extraction only adds/updates files present in the archive);npm ci && npm run build;systemctl restart "$SERVICE_NAME"and waits (up to ~40s) for/api/v1/health.
Defaults: APP_DIR=/opt/skyline-core, SERVICE_NAME=skyline,
BACKUP_DATA_DIR=<APP_DIR>/data, PORT=3020. Flags: --tarball FILE (same as the positional
arg), --skip-backup, --no-restart (build only, skip the service restart). Exit codes:
0 ok · 1 usage · 2 preflight · 3 backup failed · 4 build failed · 5 service/health
failed. See Backups for what backup.sh does.
systemctl status streamhub-core livekitjournalctl -u streamhub-core -fdocker restart ingress egressDB migrations — automatic, idempotent, backed up
Section titled “DB migrations — automatic, idempotent, backed up”Migrations run at core boot (DbService.init) — you never run them by hand. They:
- create
data/streamhub.db(global) and seed the defaultliveapp on a fresh install; - apply
GLOBAL_MIGRATIONS, idempotentGLOBAL_COLUMN_ADDSandGLOBAL_TENANCY_BACKFILL; - open each
apps/<app>/app.dblazily and applyAPP_MIGRATIONS, importing any legacyapps/<app>/vods.db(left in place as a backup); - perform the per-app split (streams/vods/ingress_auth global →
app.db) once, guarded by a marker in_streamhub_meta, after taking aVACUUM INTObackup of the global DB asstreamhub.db.bak-<timestamp>.
Everything is CREATE TABLE IF NOT EXISTS plus copy-if-absent, so re-running is safe
(redeploys, restarts, re-running the installer). Rolling forward is just: deploy the new build
and restart core — migrations self-apply.
Copy the browser SDK to /sdk
Section titled “Copy the browser SDK to /sdk”Core serves the SDK statically from SDK_DIR (default <DATA_DIR>/sdk) at
/sdk/streamhub-adaptor.global.js. On a manual deploy, place the built adaptor there:
cp streamhub-adaptor/dist/streamhub-adaptor.global.js "$DATA_DIR/sdk/"The Docker image does this during build. A missing file simply 404s — sample pages then fall
back to plain livekit-client, so this step is non-fatal but recommended.
Post-deploy smoke test
Section titled “Post-deploy smoke test”curl -s https://streamhub.example.com/api/v1/health # {"status":"ok",...}
# /metrics is denied at the vhost (403); scrape it locally on the host:curl -s http://127.0.0.1:3020/metrics | grep streamhub_
# authed:curl -s -H "Authorization: Bearer $STREAMHUB_API_TOKEN" \ https://streamhub.example.com/api/v1/stats
# Swagger UI:# https://streamhub.example.com/api/v1/docsThen exercise a real path: create an app, push RTMP
(ffmpeg … -f flv rtmp://HOST:1935/live/<key>), see it under Streams, start a recording,
and confirm the VOD lands in the app’s S3 bucket as ready with a working presigned URL.