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:
co-authored by
Claude Opus 4.8
parent
48084dd0df
commit
6bb4cf1c63
@@ -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.
|
||||||
Reference in New Issue
Block a user