Add agent-facing deployment guide (DEPLOY.md)

Self-contained, deterministic runbook for an automation agent to deploy
LiveCast from the repo to live TLS endpoints across the event and turn nodes.

- Inputs to collect, DNS/port preconditions, and per-node phases with exact
  commands.
- Verification steps with expected output at each phase, including a headless
  TURN relay check (turnutils_uclient with a minted ephemeral credential) so
  AC-3/AC-10 can be confirmed without a browser.
- Explicit split between acceptance checks an agent can complete and the
  device/network checks that must be escalated to a human.
- Idempotent re-run guidance, redeploy steps, operations table, and a
  troubleshooting matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-04 13:58:30 -05:00
co-authored by Claude Opus 4.8
parent 48084dd0df
commit 6bb4cf1c63
+346
View File
@@ -0,0 +1,346 @@
# LiveCast Deployment Guide (agent handoff)
This is a self-contained, deterministic runbook for an automation agent to take
LiveCast from the project repository to a live, TLS-secured deployment. Execute
the phases in order. Every phase ends with a verification step that has an
expected result; do not proceed until it passes.
House rule: this is infrastructure with real DNS, certificates, and a single
broadcast channel. Confirm the inputs in section 1 before running anything that
changes a host.
---
## 0. Architecture
Two nodes, both fresh Ubuntu 22.04 or 24.04 with `sudo`:
- **event node** serves `event.redclaw.dev`: Caddy (TLS, static SPA, reverse
proxy) plus the Node signaling server on internal port 8080.
- **turn node** serves `turn.redclaw.dev`: coturn only (STUN + TURN relay).
Audio is peer-to-peer (SRTP). The signaling server only brokers the handshake;
the turn node only relays media when a direct path fails.
---
## 1. Inputs to collect first
Gather these before touching any host. Substitute them everywhere they appear.
| Variable | Example | Notes |
| --- | --- | --- |
| `EVENT_HOST` | `event.redclaw.dev` | Public web host |
| `TURN_HOST` | `turn.redclaw.dev` | TURN host |
| `EVENT_IP` | from provider | Public IPv4 of the event node |
| `TURN_IP` | from provider | Public IPv4 of the turn node |
| `ACME_EMAIL` | `[email protected]` | Let's Encrypt registration/notice email |
| `REPO_URL` | `https://git.redclaw.dev/clawverse/livecast.git` | Source repo |
Access required: SSH with `sudo` on both nodes, and DNS management for
`redclaw.dev`.
---
## 2. Preconditions (verify before Phase 0)
1. DNS A records exist and resolve:
```
dig +short event.redclaw.dev # must return EVENT_IP
dig +short turn.redclaw.dev # must return TURN_IP
```
Both must return the correct IPs. Certificate issuance fails otherwise. If a
record was just created, wait for propagation before continuing.
2. Each node has outbound internet and the standard Ubuntu apt repos.
---
## 3. Port map (open via ufw in Phase 0)
| Node | Port | Proto | Purpose |
| --- | --- | --- | --- |
| event | 80 | tcp | ACME challenge, HTTP to HTTPS redirect |
| event | 443 | tcp+udp | HTTPS (Caddy) |
| turn | 80 | tcp | ACME challenge for the TURN cert |
| turn | 3478 | tcp+udp | STUN and TURN |
| turn | 5349 | tcp+udp | TURN over TLS (standard) |
| turn | 443 | tcp | TURN over TLS (firewall-traversal port) |
| turn | 49152-65535 | udp | TURN relay range |
---
## 4. Phase A: turn node
Run all of Phase A on the **turn node**.
### A1. Install and fetch code
```
sudo apt-get update
sudo apt-get install -y coturn certbot git openssl
sudo git clone https://git.redclaw.dev/clawverse/livecast.git /opt/livecast || \
sudo git -C /opt/livecast pull --ff-only
```
### A2. Firewall
```
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw allow 3478
sudo ufw allow 5349
sudo ufw allow 49152:65535/udp
sudo ufw --force enable
```
### A3. coturn config and shared secret
The secret is generated here and reused on the event node in Phase B. Store it
in your secret manager; it must match on both sides and must never be sent to a
browser.
```
SECRET=$(openssl rand -hex 32)
sudo cp /opt/livecast/deploy/turnserver.conf /etc/turnserver.conf
sudo sed -i "s/REPLACE_WITH_LONG_RANDOM_SECRET/$SECRET/" /etc/turnserver.conf
sudo sed -i 's/^#TURNSERVER_ENABLED/TURNSERVER_ENABLED/' /etc/default/coturn
printf 'TURN_SECRET=%s\n' "$SECRET" # record this for Phase B (B2)
```
### A4. TLS certificate (certbot, standalone)
coturn owns this node, so port 80 is free for certbot. The deploy hook copies
the cert into `/etc/coturn` so the unprivileged `turnserver` user can read it,
and restarts coturn now and on every renewal.
```
sudo chmod +x /opt/livecast/deploy/coturn-cert-deploy-hook.sh
sudo certbot certonly --standalone -d turn.redclaw.dev \
--non-interactive --agree-tos -m [email protected] \
--deploy-hook /opt/livecast/deploy/coturn-cert-deploy-hook.sh
sudo systemctl enable --now coturn
```
### A5. Verify (turn node)
```
# coturn is running
systemctl is-active coturn # expect: active
# cert files exist and are owned by turnserver
sudo ls -l /etc/coturn/fullchain.pem /etc/coturn/privkey.pem
# TLS listener answers on 443 with a valid turn.redclaw.dev cert
echo | openssl s_client -connect turn.redclaw.dev:443 -servername turn.redclaw.dev 2>/dev/null \
| openssl x509 -noout -subject -dates
# End-to-end relay allocation using a freshly minted ephemeral credential.
# This confirms STUN binding AND a TURN relay allocation (the AC-10 / AC-3 path)
# without a browser. $SECRET must be the value from A3; if it is not in the
# current shell, read it back from your secret store first.
U=$(( $(date +%s) + 3600 ))
C=$(printf '%s' "$U" | openssl dgst -sha1 -hmac "$SECRET" -binary | openssl base64)
turnutils_uclient -v -u "$U" -w "$C" -p 3478 turn.redclaw.dev
# Expect a successful run ending with non-zero "Total transmit" and
# "Total received" byte counters, which means a relay was allocated and used.
```
If `turnutils_uclient` is missing, install `coturn` brought it in as
`coturn-utils` on some releases: `sudo apt-get install -y coturn`.
---
## 5. Phase B: event node
Run all of Phase B on the **event node**.
### B1. Install and fetch code
```
# Node LTS (NodeSource)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
# Caddy (official apt repo)
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy git
sudo git clone https://git.redclaw.dev/clawverse/livecast.git /opt/livecast || \
sudo git -C /opt/livecast pull --ff-only
```
### B2. Firewall
```
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw --force enable
```
### B3. Signaling server
Set `SECRET` to the `TURN_SECRET` value recorded in A3 (retrieve it from your
secret store). It must be byte-for-byte identical to the turn node's secret.
```
cd /opt/livecast/server
sudo npm install --omit=dev
sudo cp -n .env.example .env
SECRET='<paste the TURN_SECRET value from A3>'
# Write production values into /opt/livecast/server/.env (PORT stays 8080).
sudo sed -i \
-e 's#^TURN_HOST=.*#TURN_HOST=turn.redclaw.dev#' \
-e 's#^TURN_REALM=.*#TURN_REALM=turn.redclaw.dev#' \
-e "s#^TURN_SECRET=.*#TURN_SECRET=${SECRET}#" \
-e 's#^TURN_TTL_SECONDS=.*#TURN_TTL_SECONDS=43200#' \
/opt/livecast/server/.env
sudo cp /opt/livecast/deploy/livecast-signaling.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now livecast-signaling
```
Verify locally before exposing it:
```
systemctl is-active livecast-signaling # expect: active
curl -s localhost:8080/health # expect JSON, broadcasterOnline:false
curl -s localhost:8080/ice # expect iceServers with a turns: 443 entry and username/credential
```
The `/ice` response must contain a `turns:turn.redclaw.dev:443?transport=tcp`
URL plus a `username` (a unix timestamp) and a `credential`. If it only returns
a `stun:` entry, `TURN_SECRET` is not set in `.env`; fix it and
`sudo systemctl restart livecast-signaling`.
### B4. Web app and Caddy
```
cd /opt/livecast/web
npm install
npm run build
sudo mkdir -p /var/www/livecast
sudo rm -rf /var/www/livecast/*
sudo cp -r dist/* /var/www/livecast/
sudo cp /opt/livecast/deploy/Caddyfile /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Caddy auto-provisions the `event.redclaw.dev` certificate via Let's Encrypt on
first request. Allow a few seconds.
### B5. Verify (event node)
```
# Static SPA over valid TLS
curl -sI https://event.redclaw.dev/ | head -n1 # expect: HTTP/2 200
# Health reachable THROUGH the proxy (confirms reverse_proxy to :8080)
curl -s https://event.redclaw.dev/health # expect status JSON
# ICE endpoint through the proxy returns ephemeral TURN creds
curl -s https://event.redclaw.dev/ice | grep -o 'turns:[^"]*' # expect the turns:443 URL
# Certificate is valid and issued for the right host
echo | openssl s_client -connect event.redclaw.dev:443 -servername event.redclaw.dev 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
```
---
## 6. Acceptance checks the agent can complete headlessly
- AC-7: `curl https://event.redclaw.dev/health` returns 200 with status JSON.
- AC-8: `/ice` `username` is a future unix timestamp; the credential changes
over time. Confirm by fetching `/ice` twice with a gap and comparing.
- AC-9: Both hosts serve valid TLS (B5 and A5 cert checks). The SPA and the
WebSocket are same-origin, so there is no mixed content.
- AC-10 / AC-3 (relay path): the `turnutils_uclient` run in A5 proves a TURN
relay allocation succeeds.
## 7. Acceptance checks that require a human (escalate)
These need real devices and networks and cannot be verified by an agent.
Report them as pending and hand off to a human tester.
- AC-1: Go live from iOS Safari and Android Chrome over cellular and Wi-Fi.
- AC-2: A listener on a different network hears audio under 1 second latency.
- AC-4: On-page LIVE indicator and browser notification within 2 seconds.
- AC-5: Stopping flips listeners to offline within 2 seconds.
- AC-6: Broadcaster reconnect restores audio without a page reload.
- Force-relay confirmation in a real browser via the WebRTC Trickle ICE tool
against `turn.redclaw.dev`, observing both `srflx` and `relay` candidates.
---
## 8. Idempotency and re-runs
- The `git clone || git pull` form in A1/B1 is safe to re-run.
- `cp -n .env.example .env` will not clobber an existing `.env`. To change env
values, edit `.env` and `sudo systemctl restart livecast-signaling`.
- Re-running A4 (`certbot certonly`) reuses the existing cert unless it is near
expiry; this is safe.
- B4 rebuilds and replaces `/var/www/livecast`; safe to re-run for redeploys.
### Redeploy after a code change
```
# event node
sudo git -C /opt/livecast pull --ff-only
cd /opt/livecast/server && sudo npm install --omit=dev && sudo systemctl restart livecast-signaling
cd /opt/livecast/web && npm install && npm run build && sudo rm -rf /var/www/livecast/* && sudo cp -r dist/* /var/www/livecast/
sudo systemctl reload caddy
```
---
## 9. Operations
| Action | Command |
| --- | --- |
| Signaling logs | `journalctl -u livecast-signaling -f` |
| Restart signaling | `sudo systemctl restart livecast-signaling` |
| coturn logs | `journalctl -u coturn -f` |
| Restart coturn | `sudo systemctl restart coturn` |
| Caddy logs | `journalctl -u caddy -f` |
| Reload Caddy | `sudo systemctl reload caddy` |
| Cert renewal status | `systemctl status certbot.timer` |
| Force cert renewal test | `sudo certbot renew --dry-run` |
Certificate renewal is automatic: certbot's systemd timer renews both certs,
and the coturn deploy hook restarts coturn with the fresh cert. Caddy renews its
own cert in-process.
---
## 10. Troubleshooting
| Symptom | Likely cause | Fix |
| --- | --- | --- |
| `/ice` returns only `stun:` | `TURN_SECRET` empty in `.env` | Set it, restart signaling |
| coturn fails to start | cert missing or unreadable | Confirm A4 ran; check `/etc/coturn/*.pem` owned by `turnserver` |
| Caddy TLS not issued | DNS not resolving or port 80 blocked | Recheck section 2 dig output and event-node ufw |
| Browser connects but no audio | TURN unreachable from client network | Confirm turn-node ufw 443/5349/3478 and relay range open |
| `502` from `/health` over HTTPS | signaling not running | `systemctl status livecast-signaling`, check logs |
| Listener never sees LIVE | signaling reachable but broadcaster not registered | Confirm `/ws` proxied (Caddy `@signaling` matcher) and broadcaster tab is live |
---
## 11. Definition of done
- A5 and B5 verifications all pass.
- Section 6 headless acceptance checks pass.
- Section 7 human checks are scheduled or handed off.
- The `TURN_SECRET` is stored in the secret manager and is identical on both
nodes.