diff --git a/.env.example b/.env.example index b6787d6..09c36e4 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,10 @@ COTURN_PASSWORD=changeme # signaling server, which in turn proxies /stream/* to ICECAST_HOST:ICECAST_PORT # # Mismatch → /stream/* returns 502 Bad Gateway. +# PUBLIC_DOMAIN is your public hostname. Only deploy/docker-compose.prod.yml +# uses it, to set the Icecast stream directory hostname. +PUBLIC_DOMAIN=studio.example.com + ICECAST_HOST=localhost ICECAST_PORT=6737 ICECAST_PASS=changeme diff --git a/.gitignore b/.gitignore index 29047dd..c51b843 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,7 @@ memory-bank/epoch/* !memory-bank/epoch/.gitkeep models/*.bin whisper.cpp/ + +# Operator-local deploy scripts — these hardcode host names, paths, and ports +# specific to one person's infrastructure. Keep them out of a public repo. +deploy.sh diff --git a/deploy/Caddyfile b/deploy/Caddyfile index 14310b2..bc61f14 100644 --- a/deploy/Caddyfile +++ b/deploy/Caddyfile @@ -1,3 +1,7 @@ -openstudio.zerologic.com { +# Example Caddy site config for OpenStudio. +# Replace the hostname with your own, then place this in /etc/caddy/sites/ +# (or wherever your Caddyfile imports from). deploy/setup.sh generates this +# for you from $DOMAIN. +studio.example.com { reverse_proxy localhost:6736 } diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index f2068cc..bf89d3f 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -6,10 +6,10 @@ services: ports: - "127.0.0.1:6737:8000" environment: - ICECAST_SOURCE_PASSWORD: "${ICECAST_PASS:?Set ICECAST_PASS}" - ICECAST_ADMIN_PASSWORD: "${ICECAST_ADMIN_PASS:?Set ICECAST_ADMIN_PASS}" - ICECAST_RELAY_PASSWORD: "${ICECAST_RELAY_PASS:?Set ICECAST_RELAY_PASS}" - ICECAST_HOSTNAME: "openstudio.zerologic.com" + ICECAST_SOURCE_PASSWORD: "${ICECAST_SOURCE_PASSWORD:?Set ICECAST_SOURCE_PASSWORD in .env}" + ICECAST_ADMIN_PASSWORD: "${ICECAST_ADMIN_PASSWORD:?Set ICECAST_ADMIN_PASSWORD in .env}" + ICECAST_RELAY_PASSWORD: "${ICECAST_RELAY_PASSWORD:?Set ICECAST_RELAY_PASSWORD in .env}" + ICECAST_HOSTNAME: "${PUBLIC_DOMAIN:?Set PUBLIC_DOMAIN to your public hostname}" restart: unless-stopped coturn: diff --git a/deploy/setup.sh b/deploy/setup.sh index 12a2e53..7b88f82 100755 --- a/deploy/setup.sh +++ b/deploy/setup.sh @@ -1,65 +1,116 @@ #!/usr/bin/env bash +# OpenStudio production setup — first-run provisioner for a dedicated host. +# +# Target: Ubuntu 22.04+ with Caddy, Docker, Node 18+, and git already installed. +# Safe to re-run: every step is idempotent and nothing is overwritten in place. +# +# This installs OpenStudio as its own system user under /opt, runs Icecast and +# coturn via Docker, and hands you a Caddy site file. It deliberately does NOT +# edit your main Caddyfile — see step 6. +# +# Usage: +# DOMAIN=studio.example.com bash deploy/setup.sh +# +# If you already run OpenStudio out of a checkout in your home directory under a +# user systemd unit, this script is not for you; deploy with git + a restart. set -euo pipefail -# OpenStudio production deployment script -# Target: Ubuntu 22.04+ with Caddy already installed +REPO_URL="${REPO_URL:-https://github.com/msitarzewski/openstudio.git}" +INSTALL_DIR="${INSTALL_DIR:-/opt/openstudio}" +SERVICE_USER="${SERVICE_USER:-openstudio}" +DOMAIN="${DOMAIN:-}" +PORT="${PORT:-6736}" -REPO_URL="https://github.com/msitarzewski/openstudio.git" -INSTALL_DIR="/opt/openstudio" -SERVICE_USER="openstudio" +info() { printf '\n== %s\n' "$*"; } +warn() { printf 'WARN: %s\n' "$*" >&2; } +fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } -echo "=== OpenStudio Production Setup ===" +[ -n "$DOMAIN" ] || fail "Set DOMAIN first, e.g. DOMAIN=studio.example.com bash deploy/setup.sh" -# Create service user -if ! id "$SERVICE_USER" &>/dev/null; then - echo "Creating user: $SERVICE_USER" - sudo useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_DIR" "$SERVICE_USER" +info "[1/7] preflight" +for cmd in git node docker caddy; do + command -v "$cmd" >/dev/null 2>&1 || fail "Missing required command: $cmd" +done +NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" +[ "$NODE_MAJOR" -ge 18 ] || fail "Node 18+ required (found $(node -v))" +docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 plugin required" +echo " ok: git, node $(node -v), docker, caddy" + +info "[2/7] service user" +if id "$SERVICE_USER" &>/dev/null; then + echo " user $SERVICE_USER already exists" +else + sudo useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_DIR" "$SERVICE_USER" + echo " created $SERVICE_USER" fi -# Clone or update repo -if [ -d "$INSTALL_DIR" ]; then - echo "Updating existing installation..." - cd "$INSTALL_DIR" - sudo -u "$SERVICE_USER" git pull +info "[3/7] code" +if [ -d "$INSTALL_DIR/.git" ]; then + echo " updating existing checkout" + sudo -u "$SERVICE_USER" git -C "$INSTALL_DIR" diff --quiet \ + || fail "$INSTALL_DIR has local modifications; resolve them before re-running" + sudo -u "$SERVICE_USER" git -C "$INSTALL_DIR" fetch --prune origin + sudo -u "$SERVICE_USER" git -C "$INSTALL_DIR" merge --ff-only origin/main +elif [ -e "$INSTALL_DIR" ]; then + fail "$INSTALL_DIR exists but is not a git checkout; move it aside first" else - echo "Cloning repository..." - sudo git clone "$REPO_URL" "$INSTALL_DIR" - sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" + sudo git clone "$REPO_URL" "$INSTALL_DIR" + sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" fi -# Install dependencies -echo "Installing dependencies..." -cd "$INSTALL_DIR" -sudo -u "$SERVICE_USER" bash -c "cd server && npm install --production" +info "[4/7] dependencies" +sudo -u "$SERVICE_USER" bash -c "cd '$INSTALL_DIR/server' && npm ci --omit=dev" + +info "[5/7] station manifest" +if [ -f "$INSTALL_DIR/station-manifest.json" ]; then + echo " station-manifest.json exists — leaving your settings alone" +else + sudo -u "$SERVICE_USER" cp "$INSTALL_DIR/deploy/station-manifest.production.json" \ + "$INSTALL_DIR/station-manifest.json" + warn "station-manifest.json ships with CHANGE_ME TURN credentials and example" + warn "STUN/TURN hostnames. Edit it before going live or WebRTC will fail for" + warn "anyone behind a symmetric NAT." +fi -# Copy production station manifest -echo "Setting up station manifest..." -sudo -u "$SERVICE_USER" cp deploy/station-manifest.production.json station-manifest.json +info "[6/7] Caddy site config (never edits your main Caddyfile)" +CADDY_BLOCK="$(printf '%s {\n\treverse_proxy localhost:%s\n}\n' "$DOMAIN" "$PORT")" +if grep -rqs -- "$DOMAIN" /etc/caddy/ 2>/dev/null; then + echo " $DOMAIN already present in /etc/caddy — leaving it alone" +elif [ -d /etc/caddy/sites ]; then + printf '%s' "$CADDY_BLOCK" | sudo tee "/etc/caddy/sites/${DOMAIN}.caddy" >/dev/null + echo " wrote /etc/caddy/sites/${DOMAIN}.caddy" + echo " (requires 'import sites/*.caddy' in your main Caddyfile)" + sudo systemctl reload caddy +else + warn "No /etc/caddy/sites directory, and appending to a shared Caddyfile could" + warn "break other sites on this host. Add this block yourself, then reload Caddy:" + printf '\n%s\n' "$CADDY_BLOCK" +fi -# Start Docker services (Icecast + coturn) -echo "Starting Docker services..." -cd "$INSTALL_DIR/deploy" -sudo docker compose -f docker-compose.prod.yml up -d +info "[7/7] services" +# Icecast + coturn. Passwords come from the environment — see .env.example. +( cd "$INSTALL_DIR/deploy" && sudo -E docker compose -f docker-compose.prod.yml up -d ) -# Install systemd service -echo "Installing systemd service..." sudo cp "$INSTALL_DIR/deploy/openstudio.service" /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable openstudio sudo systemctl restart openstudio -# Configure Caddy -echo "Configuring Caddy..." -if ! grep -q "openstudio.zerologic.com" /etc/caddy/Caddyfile 2>/dev/null; then - sudo cat "$INSTALL_DIR/deploy/Caddyfile" >> /etc/caddy/Caddyfile - sudo systemctl reload caddy - echo "Caddy configured with TLS" -else - echo "Caddy already configured for openstudio.zerologic.com" -fi +for i in $(seq 1 30); do + curl -sf --max-time 3 "http://localhost:${PORT}/health" >/dev/null 2>&1 && break + [ "$i" -eq 30 ] && fail "Service did not become healthy; check: sudo journalctl -u openstudio -n 50" + sleep 1 +done + +cat <