#!/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."