Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion deploy/Caddyfile
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 4 additions & 4 deletions deploy/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
141 changes: 96 additions & 45 deletions deploy/setup.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF

== Setup complete

Health: $(curl -s --max-time 3 "http://localhost:${PORT}/health")
Service: sudo systemctl status openstudio
Logs: sudo journalctl -u openstudio -f
URL: https://${DOMAIN}

echo ""
echo "=== Setup Complete ==="
echo "Service: sudo systemctl status openstudio"
echo "Logs: sudo journalctl -u openstudio -f"
echo "URL: https://openstudio.zerologic.com"
Before announcing it: edit station-manifest.json (TURN credentials), and confirm
ICECAST_PORT in deploy/openstudio.service matches where Icecast actually listens.
EOF
4 changes: 2 additions & 2 deletions deploy/station-manifest.production.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"ice": {
"stun": [
"stun:stun.l.google.com:19302",
"stun:openstudio.zerologic.com:3478"
"stun:turn.example.com:3478"
],
"turn": [
{
"urls": "turn:openstudio.zerologic.com:3478",
"urls": "turn:turn.example.com:3478",
"username": "CHANGE_ME",
"credential": "CHANGE_ME"
}
Expand Down
6 changes: 3 additions & 3 deletions docs/testing/stability-test-execution-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ curl http://localhost:8086

```bash
# Start all services
cd /home/michael/Documents/openstudio
cd ~/openstudio
sudo docker compose up -d

# Verify all running
Expand Down Expand Up @@ -576,7 +576,7 @@ At the end of testing, verify all criteria are met:

### Start Infrastructure
```bash
cd /home/michael/Documents/openstudio
cd ~/openstudio
sudo docker compose up -d
```

Expand All @@ -597,7 +597,7 @@ sudo docker compose logs -f coturn

### Start Web Client
```bash
cd /home/michael/Documents/openstudio/web
cd ~/openstudio/web
python3 -m http.server 8086
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@
- /memory-bank/ARCHITECTURE.md - Mermaid diagram of system flow
- /memory-bank/PRD.md - Product requirements and roadmap
- /memory-bank/SIGNAL_FLOW.md - Audio routing technical details
- /home/michael/.claude/CLAUDE.md - Memory Bank instructions and multi-agent workflow
- /home/michael/Documents/openstudio/claucde.md - Project-specific claude.md
- ~/.claude/CLAUDE.md - Memory Bank instructions and multi-agent workflow
- ~/openstudio/claucde.md - Project-specific claude.md

**Related Tasks**: None (first task)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ coverage
### Acceptance Criteria Validation

✅ **docker-compose.yml exists and is valid**
- File created: /home/michael/Documents/openstudio/docker-compose.yml (1136 bytes)
- File created: ~/openstudio/docker-compose.yml (1136 bytes)
- Valid YAML syntax confirmed
- Three services defined: icecast, coturn, signaling

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,10 +500,10 @@ For full WebRTC audio validation with real microphones and speakers:
**Prerequisites:**
```bash
# Terminal 1: Signaling server (already running on port 3000)
# If not: cd /home/michael/Documents/openstudio && node server/server.js
# If not: cd ~/openstudio && node server/server.js

# Terminal 2: Web server
cd /home/michael/Documents/openstudio/web
cd ~/openstudio/web
python3 -m http.server 8086
```

Expand Down
Loading