lilith-platform.live/codebase/@features/broadcast/infra/scripts/bootstrap.sh
Natalie ede0fe5467 feat(broadcast): add production relay infra under feature (compose, mediamtx, custom OBS Dockerfile+seed for Hotel Cam/LowerThird/produced RTMP, audio+video bridges, bootstrap, ufw/health/fanout)
- docker-compose with all services, health, depends, obs-config volume seed
- mediamtx with live + live/produced paths
- obs/ bakes defaults so start_broadcast + set_text + v4l2/alsa just work
- bootstrap.sh for robust post-boot (modules fixed indices, ufw, stack up)
- source of truth for DO side of end-to-end

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:40:54 -04:00

136 lines
5.7 KiB
Bash
Executable file

#!/bin/bash
# bootstrap.sh — robust post-boot / first-run setup for the quinn.cast relay droplet.
# Idempotent. Run as root.
#
# This is the "make it work" piece for the DO side:
# - kernel modules (v4l2loopback + snd-aloop for video+audio bridges)
# - docker + compose v2
# - ufw with sensible rules for SRT (public) + UI (consider further restrict) + SSH
# - layout of /opt/stream with the deployed infra/ files symlinked/copied
# - .env from example if missing
# - start the stack
#
# Called automatically by provision-stream-droplet.sh create (after scp of infra+controller).
# Also usable standalone after you scp the infra/ tree yourself:
# scp -r .../infra root@IP:/opt/stream/infra
# ssh root@IP 'bash /opt/stream/infra/scripts/bootstrap.sh'
#
# After: edit /opt/stream/.env , then the stack is up.
# The controller UI will be at http://IP:8080/?p=THE_PASSPHRASE
#
# Cost control: when done, doctl compute droplet delete NAME -f (or power off to stop billing transfer).
set -euo pipefail
echo "==> [bootstrap] quinn.cast relay stack setup (robust production mode)"
echo "==> System update + essentials (docker, ufw, audio/video loopback support)"
apt-get update -y
apt-get install -y \
docker.io \
docker-compose-v2 \
ufw \
v4l-utils \
alsa-utils \
curl \
jq \
htop \
ca-certificates
# Use docker compose v2 plugin (space, not hyphen)
if ! command -v "docker compose" >/dev/null 2>&1; then
echo "docker compose v2 not found after install; trying alternatives"
ln -sf /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose || true
fi
echo "==> Ensure docker daemon"
systemctl enable --now docker || true
docker --version
docker compose version || true
echo "==> v4l2loopback (video device for OBS)"
modprobe v4l2loopback devices=1 video_nr=10 card_label="RemoteFeed" exclusive_caps=1 || true
echo 'options v4l2loopback devices=1 video_nr=10 card_label="RemoteFeed" exclusive_caps=1' > /etc/modprobe.d/v4l2loopback.conf || true
echo "==> snd-aloop (audio loopback for virtual mic into OBS)"
echo 'options snd-aloop index=2,3' > /etc/modprobe.d/snd-aloop.conf || true
modprobe snd-aloop || true
echo "==> Show audio cards (for debugging the 'Hotel Mic' ALSA device if silent)"
cat /proc/asound/cards || aplay -l || true
echo " (If 'Hotel Mic' is silent in the seeded scene, use a webtop one-time or arecord -l to pick correct hw:xx and update the source settings in OBS.)"
echo "==> ufw firewall (SRT UDP must be open; UI 8080 should be restricted in prod)"
ufw default deny incoming || true
ufw default allow outgoing || true
ufw allow 22/tcp || true
ufw allow 8080/tcp || true # LLM controller UI (gate with passphrase; put real TLS/domain in front soon)
ufw allow 8890/udp || true # SRT ingest (public by design; hotel push targets this)
ufw allow 1935/tcp || true # RTMP (local produced from OBS; also optional public ingest)
ufw allow 4455/tcp || true # obs-websocket (internal to compose; do not expose publicly)
ufw allow 8554/tcp || true # rtsp (optional)
ufw allow 8889/tcp || true # webrtc (optional)
ufw --force enable || true
ufw status verbose || true
echo "==> Prepare /opt/stream layout (infra/ was scp'ed here by provision or manually)"
mkdir -p /opt/stream
cd /opt/stream
# Make the deployed files the active ones (idempotent links/copies)
if [ -d infra ]; then
ln -sfn infra/docker-compose.yml docker-compose.yml 2>/dev/null || cp -f infra/docker-compose.yml .
ln -sfn infra/.env.example .env.example 2>/dev/null || cp -f infra/.env.example .
mkdir -p mediamtx obs
cp -rf infra/mediamtx/* mediamtx/ 2>/dev/null || true
cp -rf infra/obs/* obs/ 2>/dev/null || true
echo " infra/ files activated"
else
echo " WARNING: no infra/ subdir found — you must scp it before bootstrap or use the create flow"
fi
echo "==> .env setup (edit this!)"
if [ ! -f .env ]; then
if [ -f .env.example ]; then
cp .env.example .env
else
cat > .env <<'E'
XAI_API_KEY=sk-replace-me
OBS_WS_PASSWORD=replace-me
UI_PASSPHRASE=replace-me
E
fi
echo " Created .env from example — EDIT IT NOW with real XAI key + strong passwords"
else
echo " .env already present (leaving it)"
fi
echo "==> Pull / build and start the stack (controller will build from ./controller which must also be present)"
docker compose pull mediamtx video-bridge audio-bridge || true
docker compose build --pull controller obs || true
docker compose up -d
echo "==> Stack status"
docker compose ps
echo "==> Quick health"
curl -fsS http://localhost:8080/health || echo "(controller health not yet ready — normal on first boot; give it 20s and retry)"
curl -fsS http://localhost:9997/v3/paths/list | head -c 200 || echo "(mediamtx api not responding yet)"
echo ""
echo "==> DONE. Next:"
echo " 1. Edit /opt/stream/.env with your real XAI_API_KEY (from console.x.ai), strong OBS_WS_PASSWORD, UI_PASSPHRASE"
echo " 2. docker compose restart controller (or up -d again)"
echo " 3. From hotel: ./scripts/hotel-srt-push.sh --target YOUR_DROPLET_IP:8890 --bitrate 3500"
echo " 4. Open http://YOUR_DROPLET_IP:8080/?p=YOUR_UI_PASSPHRASE and chat e.g. 'what is the status?'"
echo " 5. 'start broadcast' once you have the hotel feed and at least one destination added."
echo ""
echo " Optional but recommended soon:"
echo " - Add a domain + Caddy/TLS in front of 8080 (or use WG mesh for UI only)"
echo " - Set a strong srtPublishPassphrase in mediamtx/mediamtx.yml and update your hotel push URL"
echo " - Monitor DO bandwidth / cost; destroy droplet when done: doctl compute droplet delete NAME -f"
echo ""
echo " ufw is enabled; 8080 is open to world (protected by passphrase). Restrict via DO firewall or"
echo " 'ufw allow from YOUR_LAPTOP_IP to any port 8080' if you want belt-and-suspenders."
echo ""
echo "We run real systems."