Files
Media-Server-Setup/FRESH-INSTALL.md
Brandyn / Techy ef357c6d82 Docs follow the Elastic swap, and apply removes orphaned containers
README 5h, Phase 13, FRESH-INSTALL 7b and appendix M, and STATUS all describe four Elastic containers with
Kibana as the single pane instead of the Victoria shape. Phase 13 keeps the wrong turn on the record: the
first build had no dashboards, which was the point of the exercise, and Grafana was rejected for this purpose
because it is a UI only and its stack is the same piece count with more parts.

configure.py apply now passes --remove-orphans, which is how the eight containers the compose file no longer
lists actually go away rather than lingering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 16:58:08 -06:00

374 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Fresh install — from an empty OVH box to a working stack with the deploy pipeline
Everything in order. Scripted steps are commands; manual steps are click lists. `HOST` = your main hostname
(ours: `media.dumpstervigilantes.rip`), `BASE` = the parent domain. Detailed background for each part is in
README.md (section numbers given).
## 0. Before touching the server
- DNS: `A` record for `HOST` → the server IP; `CNAME`s `files`, `clips`, `sync`, `live`, `diag`, `auth`, `watch`, `git`, `bot` → `HOST`.
Optional: the bare domain `A` → server IP (it redirects to `https://HOST/`).
- The Discord bot lives in its **own** repo (ours `VanguardOrg/VanguardBot`), not in this one. Nothing here
needs it to exist; section 7a wires it up afterwards.
- Discord developer portal → New Application (one app serves Authentik login, the Jellyfin plugin's bot and the
Discord bot):
- OAuth2 → Redirects: `https://auth.BASE/source/oauth/callback/discord/` (the last segment is the Authentik
source slug — keep them identical). Copy Client ID + Client Secret.
- Bot → Reset Token (copy it); enable **Server Members** intent; OAuth2 URL generator: scope `bot`, permissions
View Channels + Send Messages + Embed Links + Attach Files → invite it to the server.
- Discord → User Settings → Advanced → Developer Mode on; right-click the server → Copy Server ID; Server
Settings → Roles → Copy Role ID for each role you will map to a group.
- If this is a **rebuild**, have the backups ready: `.env`, `/srv/appdata`, `/srv/media`, `/etc/vanguard/clip-quotas.json`.
Restoring `.env` + `/srv/appdata` makes sections 5–7 unnecessary (that state lives there).
## 1. OS (README §1)
OVH manager → Reinstall → Debian 13, custom partitioning, distribution kernel. RAID 1:
`/boot` 1 GB ext4 · `/` 250 GB ext4 · swap 8 GB per disk · `/srv` rest xfs. Add your SSH key. You get the `debian` user with sudo.
## 2. Code onto the box
Fresh box has no deploy key yet, so copy the repo in:
scp -r vanguard-stream debian@HOST:~/ # from a machine that has the repo (or git clone over https with a token)
## 3. Host install (README §2, §4)
ssh debian@HOST
cd ~/vanguard-stream && sudo bash install.sh
Packages, Docker, timezone (Eastern), uid 1000, `/srv` folders, host scripts and timers, sysctl, live-portal
build, **firewall** (keep this session open; test a new ssh in a second terminal). Log out and back in once (docker group).
## 4. Configure + start (README §3)
cd ~/vanguard-stream && ./configure.py --full
Asks: main hostname (the gateway page); `files`/`clips`/`sync`/`diag`/`auth`/`watch`/`git`/`bot`/`live` hostnames (defaults); bare domain (optional) and where it
redirects; Discord bot token + server ID (optional now, needed for the plugin feed); "is live" webhook (optional).
Generates `.env` with every secret, the configs, starts all containers, waits, then runs the post-start fixups
(Syncthing GUI auth off, Gitea admin user, Jellyfin plugin). `./configure.py --show` prints every credential
you will need below (akadmin password, Gitea admin, MariaDB, bot DB).
Check: `docker compose ps` → all `Up`; `https://auth.BASE/`, `https://watch.BASE/`, `https://git.BASE/`,
`https://HOST/` answer with certificates (Caddy fetches them itself, give it a minute).
## 5. Authentik (README §5c, §5g) — manual, ~20 minutes
Log in at `https://auth.BASE/` as `akadmin` with `AUTHENTIK_BOOTSTRAP_PASSWORD` from `.env` (change it: user menu → settings).
1. Directory → Federation & Social login → Create → **Discord OAuth Source**: name `Discord`, slug `discord`,
consumer key/secret = the Discord app's Client ID/Secret, scopes `guilds guilds.members.read`, user matching
"link on unique identifier", flows default-source-authentication / default-source-enrollment.
2. Flows & Stages → Stages → `default-authentication-identification` → Sources: add Discord, tick "Show sources'
labels". (No Discord button appears until this is done.)
3. Flows & Stages → Stages → `default-source-enrollment-write` → User type **Internal**, user path `users`.
4. Directory → Groups: `streamers`, `editors`, `media-admins`, `admins`, `bot-users`, `git-users`, `git-admins`.
Nesting: `admins` parent `authentik Admins`; `authentik Admins` parent `git-admins` (admins inherit everything).
Inherited groups reach the apps only with the extra scope mapping `Vanguard profile (nested groups)` (README 5e),
ticked on the `Vanguard sites`, `Jellyfin` and `Gitea` providers next to the default profile mapping.
5. Customization → Policies → Create Expression Policy ×2 from the repo:
`discord-guild-member` (`authentik/discord-guild-policy.py`, set your server ID) and
`discord-roles-to-groups` (`authentik/discord-roles-to-groups.py`, set server ID + role-id→group table).
Tick "Execution logging" on both.
6. Flows & Stages → Flows → `default-source-enrollment` and `default-source-authentication`: bind both policies
(guild first), **Policy engine mode: all**.
7. Applications → Outposts → edit "authentik Embedded Outpost" → config `authentik_host: https://auth.BASE`.
8. Applications → Create with provider → `Vanguard sites`: **Proxy Provider**, implicit consent,
**Forward auth (domain level)**, authentication URL `https://auth.BASE`, cookie domain `BASE`,
invalidation flow `default-provider-invalidation-flow`. Edit the Embedded Outpost → add the application.
9. Applications → Create with provider → `Jellyfin`: OAuth2/OpenID, implicit consent, Confidential, redirect
`https://watch.BASE/sso/OID/redirect/authentik`, default signing cert, scopes `openid email profile`. Keep ID + secret.
10. Applications → Create with provider → `Gitea` (slug `gitea`): OAuth2/OpenID, same settings, redirect
`https://git.BASE/user/oauth2/authentik/callback`. Bind groups `git-users` + `git-admins` on the application.
11. Test in a private window: `https://auth.BASE/` → Discord → your user exists and is Internal. Add yourself to
`admins` (and `streamers`, `media-admins` as needed) or give yourself the Discord roles; sign out and back in.
`https://HOST/whoami` shows your groups.
## 6. Jellyfin (README §5c) — manual, ~10 minutes
`https://watch.BASE/` → wizard: admin user; libraries `/media/streams` and `/media/clips` as **Shows** with
metadata downloaders = only "Vanguard Metadata" (Series/Season/Episode), image fetchers Vanguard Metadata
(Series) + Screen Grabber (Episode), real-time monitoring on; `/media/finals` as Home Videos, fetchers off.
Dashboard → Playback → Transcoding: hardware acceleration none. Dashboard → Plugins: Vanguard Metadata is
installed → its page: Discord bot token + server ID, feed channel ID, public URL `https://watch.BASE`.
Plugins → Repositories → add `https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/manifest-release/manifest.json`
→ Catalog → SSO Authentication → install → `docker compose restart jellyfin`. Plugins → SSO-Auth → add OID provider
`authentik`: endpoint `https://auth.BASE/application/o/jellyfin/`, client ID/secret, roles claim `groups`,
admin roles `admins`, enabled folders = the 3 libraries, Scheme Override `https`, Port Override EMPTY, Save.
Dashboard → General → Branding: paste `jellyfin/branding-disclaimer.html` (set the hostname) and `jellyfin/branding.css`.
Test: `https://watch.BASE/sso/OID/start/authentik`. Scheduled Tasks → run "Vanguard: snapshot Discord events",
"roll seasons", "enforce playback policy" once.
## 7. Gitea (README §5e) — manual, ~10 minutes
Log in at `https://git.BASE/` as `GITEA_ADMIN_USER` / `GITEA_ADMIN_PASSWORD` from `.env`.
1. Site Administration → Identity & Access → Authentication Sources → Add: OAuth2, name `authentik`, OpenID
Connect, client ID/secret from step 5.10, discovery URL
`https://auth.BASE/application/o/gitea/.well-known/openid-configuration`, Skip local 2FA, group claim
`groups`, administrator group `git-admins`.
2. Create the repo `techy/Media-Server-Setup` (empty) and push this repo to it from your dev box:
`git remote add origin ssh://git@git.BASE:2222/techy/Media-Server-Setup.git && git push -u origin main`
(your SSH key under Settings → SSH keys).
3. Runner: `./registerGiteaRunner.sh` on the server → Site Administration → Actions → Runners shows it idle.
## 7a. Discord bot (README 5d)
1. The bot is **not** part of this repo. Its code arrives as a release bundle built by its own Gitea Actions
workflow and installed by `deployBotBundle.sh`; the image here carries no bot dependencies.
2. Create the org + repo in Gitea (ours: `VanguardOrg/VanguardBot`) and let the dev push to it.
3. The runner needs the **`python312`** label (CPython 3.12 + node, for the JavaScript actions). It is in
`docker-compose.yml`; labels are fixed at registration, so `./registerGiteaRunner.sh` after any change.
4. In Gitea → the bot repo → Settings → Actions: secrets `BOT_DEPLOY_SECRET` (from `./configure.py --show`)
and `DISCORD_BOT_TOKEN`; variable `DISCORD_GUILD_ID`. **The bot's secrets live here, not in `.env`.**
5. Authentik: a Proxy Provider for `https://bot.BASE` on the embedded outpost (same shape as `live.`).
6. Push to `main` → CI tests, builds the bundle, posts it signed to dumpd, which installs it and restarts the
container. By hand: `./deployBotBundle.sh <bundle.zip>`; back out with `--rollback`.
## 7b. Diagnostics — Kibana (README §5h)
1. The `diag` CNAME must exist first. Four containers come up from `configure.py`: Elasticsearch (logs AND
metrics), Kibana (the whole `diag.BASE` host, at the root), Filebeat and Metricbeat. Nothing is published
except through the gate (`DIAG_REQUIRE_GROUP`, `media-admins`).
2. `install.sh` and every apply set `vm.max_map_count=262144`. Elasticsearch will not start without it.
3. `ALERT_WEBHOOK_URL` in `.env` is optional. Without it Kibana rules still evaluate and the deadman still
logs; only delivery is missing.
4. Three first-run actions inside Kibana, all UI so they stay adjustable: the retention delete phase, the
always-firing heartbeat rule the deadman watches, and a webhook connector for alerts. README §5h lists them.
5. Adding this host to a RUNNING server needs `--full` or `--hosts`, not `--apply` (HOSTNAME-CHANGE.md §D).
## 8. Pipeline (README "Deployment")
1. On the server: `./switchToClone.sh key` → add the printed key as a read-only **Deploy Key** on the repo
→ `./switchToClone.sh clone` (keeps `.env`, generated configs, plugin zips) → `./configure.py --apply`.
2. Gitea → repo → Settings → Webhooks → Add → Gitea: URL `http://<docker gateway>:8081/deploy`
(`docker network inspect vanguard-stream_default -f '{{(index .IPAM.Config 0).Gateway}}'`), secret =
`DEPLOY_WEBHOOK_SECRET` from `.env`, push events, branch `main`. Test delivery → 202 "auto-deploy disabled".
3. Manual deploys from now on: push, then `sudo vanguard-deploy pull` on the server. When trusted:
`DEPLOY_AUTO=1` in `.env` → `./configure.py --apply`; CI: `git mv .gitea/draft-workflows/ci.yml .gitea/workflows/ci.yml`.
## 9. Streamers, gamers, editors (README §5, §5a, §6)
- Streamers: OBS → WebSocket server on; Docks → Custom Browser Dock `https://live.BASE/dock/` → Discord login →
Settings: OBS WebSocket password; Timecode band → "Add band + clock source" (not streaming). Then title → Go Live.
- Gamers: `https://clips.BASE/` → paste their Syncthing Device ID → accept device + folder on their PC (Send Only).
Syncthing UI (`https://sync.BASE/`, media-admins): Actions → Settings → Connections → incoming rate limit 12500 KiB/s.
- Everyone: `https://HOST/` (the gateway page; editors browse at `https://files.BASE/`).
## 10. Afterwards
- Pin `AUTHENTIK_TAG` in `.env` to the running version. Rotate any secret that was ever pasted into chat.
- Back up `.env`, `/srv/appdata`, `/srv/media` (home archive puller: README §7).
- `STATUS.md` = what runs and what is unfinished; `check.sh` before any commit.
---
# Appendix — the runbook: every action, in order, with the exact commands
Conventions: commands prefixed `dev$` run on your workstation (it has the repo at `~/claudevs/vanguard-stream`
and your SSH key); `srv$` run on the server over ssh as `debian`. Replace `HOST` / `BASE` with your names
(ours: `media.dumpstervigilantes.rip` / `dumpstervigilantes.rip`). Click paths are written `App → Menu → Item`.
Every step ends with how you know it worked.
## A. DNS and Discord (no server yet)
A1. DNS at your registrar:
A HOST → <server IP>
CNAME auth.BASE → HOST
CNAME watch.BASE → HOST
CNAME git.BASE → HOST
CNAME bot.BASE → HOST
CNAME live.BASE → HOST
(optional) A BASE → <server IP>
Check from dev: dev$ for h in HOST auth.BASE watch.BASE git.BASE bot.BASE live.BASE; do printf '%-28s ' $h; getent hosts $h | awk '{print $1}'; done
→ every line shows the server IP.
A2. Discord Developer Portal → New Application "Vanguard".
OAuth2 → Redirects → add https://auth.BASE/source/oauth/callback/discord/ → Save. Copy Client ID and (Reset) Client Secret.
Bot → Reset Token → copy. Privileged Gateway Intents → Server Members Intent ON → Save.
OAuth2 → URL Generator → scopes: bot; permissions: View Channels, Send Messages, Embed Links, Attach Files → open the URL → invite the bot to your server.
→ the bot appears in your server's member list.
A3. Discord IDs: User Settings → Advanced → Developer Mode ON. Right-click your server → Copy Server ID.
Server Settings → Roles → right-click each role you'll map → Copy Role ID. Keep a note like:
guild=123… streamers=… editors=… media-admins=… admins=… bot-users=… git-users=… git-admins=…
## B. Operating system
B1. OVH manager → the server → Reinstall → Debian 13 → custom partitioning, "use distribution kernel":
/boot 1 GiB ext4 (RAID1) · / 250 GiB ext4 (RAID1) · swap 8 GiB (per disk) · /srv remaining xfs (RAID1).
Add your SSH public key. Wait for the mail.
B2. dev$ ssh debian@HOST 'lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT; df -h /srv'
→ /srv is xfs, ~3.4 TiB, mounted.
## C. Code onto the box
C1. dev$ cd ~/claudevs && scp -r vanguard-stream debian@HOST:~/
C2. dev$ ssh debian@HOST 'ls ~/vanguard-stream | head; test -f ~/vanguard-stream/install.sh && echo ok'
→ "ok".
## D. Host install
D1. dev$ ssh -t debian@HOST 'cd ~/vanguard-stream && sudo bash install.sh'
(~5 min: packages, docker, timezone, folders, scripts, timers, portal build, firewall.) KEEP THIS TERMINAL OPEN.
D2. In a SECOND terminal: dev$ ssh debian@HOST 'echo still in' → "still in" (firewall did not lock you out).
D3. dev$ ssh debian@HOST 'timedatectl show -p Timezone --value; systemctl is-active docker dumpd normalize-sweep.timer trash-purge.timer nftables; docker --version; node --version; ls /srv/live | head -3'
→ America/New_York; five "active"; versions; /srv/live has index.html.
D4. Log out/in once for the docker group: dev$ ssh debian@HOST 'docker ps >/dev/null && echo docker-ok' → docker-ok.
## E. Configure and start the stack
E1. dev$ ssh -t debian@HOST 'cd ~/vanguard-stream && ./configure.py --full'
Answers: HOST; Enter ×8 for the sub-hostnames; bare domain (Enter = BASE, or blank to skip) and its redirect;
bot token (from A2) ; server ID (A3) ; "is live" webhook (optional); y to apply.
(Pulls ~2 GB of images, starts 13 containers, waits, runs post-start fixups.)
E2. dev$ ssh debian@HOST 'cd ~/vanguard-stream && docker compose ps --format "{{.Name}} {{.Status}}"'
→ every container "Up" (authentik-server/worker take ~1 min; act_runner restarts until step G3 — fine).
E3. dev$ for h in HOST auth.BASE watch.BASE git.BASE live.BASE bot.BASE; do printf '%-26s ' $h; curl -s -o /dev/null -m 10 -w '%{http_code}\n' https://$h/; done
→ 200/302 for all (302 = redirected to login). If 000, wait a minute for certificates and retry.
E4. dev$ ssh debian@HOST 'cd ~/vanguard-stream && ./configure.py --show'
→ copy out: akadmin password (AUTHENTIK_BOOTSTRAP_PASSWORD), Gitea admin user/password, MariaDB root + bot + gitea passwords.
E5. dev$ ssh debian@HOST 'journalctl -u dumpd -n 2 --no-pager | tail -1; grep -c "authMethod: http" ~/vanguard-stream/mediamtx.yml; sudo nft list table inet vanguard | grep -c dport'
→ dumpd line ending "token-secret=ok"; 1; a dozen rules.
## F. Authentik (browser, logged in as akadmin at https://auth.BASE/ with the E4 password)
F1. Change akadmin's password: top-right user → Settings → Change password.
F2. Directory → Federation & Social login → Create → "Discord OAuth Source":
Name Discord · Slug discord · Enabled · User matching "Link users on unique identifier" ·
Consumer key = Client ID · Consumer secret = Client Secret · Scopes: guilds guilds.members.read ·
(Flow settings if shown: authentication default-source-authentication, enrollment default-source-enrollment) → Finish.
F3. Flows & Stages → Stages → default-authentication-identification → Edit → Sources: select Discord; tick "Show sources' labels" → Update.
→ the login page (private window) now shows a Discord button.
F4. Flows & Stages → Stages → default-source-enrollment-write → Edit → User type: Internal · User path: users → Update.
F5. Directory → Groups → Create, one each: streamers, editors, media-admins, admins, bot-users, git-users, git-admins.
Then nesting: Groups → admins → Edit → Parent: authentik Admins → Update. Groups → authentik Admins → Edit → Parent: git-admins → Update.
F5b. Customization → Property Mappings → Create → Scope Mapping: Name `Vanguard profile (nested groups)`, Scope name `profile`, Expression:
direct = set(request.user.ak_groups.all())
return {"groups": sorted(g.name for g in request.user.all_groups() if g not in direct)}
→ Finish. Later, on each provider created in F7–F9 (Vanguard sites, Jellyfin, Gitea) → Edit → Advanced protocol settings → Scopes: tick it
next to the default profile mapping (proxy providers re-add the default on save; that is fine, the two merge). Without it `/whoami` never shows git-admins.
F6. Customization → Policies → Create → Expression Policy:
name discord-guild-member · expression = contents of authentik/discord-guild-policy.py with ACCEPTED_GUILD_ID + GUILD_NAME filled in · tick Execution logging → Create.
Again: name discord-roles-to-groups · expression = authentik/discord-roles-to-groups.py with GUILD_ID + ROLE_TO_GROUP filled from A3 · Execution logging → Create.
F7. Flows & Stages → Flows → default-source-enrollment → Policy / Group / User Bindings → Bind existing policy → discord-guild-member (order 0) → Create;
again → discord-roles-to-groups (order 10) → Create. Then Edit the flow → Policy engine mode: all → Update.
Repeat all of F7 for default-source-authentication.
F8. Applications → Outposts → "authentik Embedded Outpost" → Edit → in the configuration box set
authentik_host: https://auth.BASE (leave authentik_host_browser empty) → Update.
F9. Applications → Create with provider:
Application name "Vanguard sites" · Provider type Proxy Provider · Authorization flow default-provider-authorization-implicit-consent ·
Mode "Forward auth (domain level)" · Authentication URL https://auth.BASE · Cookie domain BASE ·
Invalidation flow default-provider-invalidation-flow → Finish.
Applications → Outposts → Embedded Outpost → Edit → Applications: add "Vanguard sites" → Update.
F10. Applications → Create with provider: "Jellyfin" (slug jellyfin) · OAuth2/OpenID Provider · implicit-consent ·
Client type Confidential · Redirect URI (strict) https://watch.BASE/sso/OID/redirect/authentik ·
Signing key: the default certificate · Advanced → Scopes: openid, email, profile → Finish. Note Client ID + Secret.
F11. Applications → Create with provider: "Gitea" (slug gitea) · same provider settings · Redirect URI https://git.BASE/user/oauth2/authentik/callback → Finish.
Note Client ID + Secret. Applications → Gitea → Policy / Group / User Bindings → Bind existing group → git-users → Create; again → git-admins.
F12. Private window → https://auth.BASE/ → Discord → log in. Admin: Directory → Users → your user exists, type Internal.
Directory → Groups → admins → Users tab → Add existing user → you (and media-admins, streamers as you like) — or give yourself the Discord roles.
F13. Private window → https://HOST/whoami → "user: <you>" and "groups: …" listing what you added. (If groups are missing: sign out at https://auth.BASE/if/flow/default-invalidation-flow/ and log in again.)
Events → Logs → filter policy_execution → entries for both policies with result pass.
## G. Gitea (browser at https://git.BASE/, log in with GITEA_ADMIN_USER / GITEA_ADMIN_PASSWORD from E4)
G1. Site Administration → Identity & Access → Authentication Sources → Add Authentication Source:
Type OAuth2 · Name authentik · Provider OpenID Connect · Client ID / Secret from F11 ·
OpenID Connect Auto Discovery URL https://auth.BASE/application/o/gitea/.well-known/openid-configuration ·
Skip local 2FA ✓ · Additional scopes (blank) · Claim name providing group names: groups · Group claim value for administrator users: git-admins → Add.
G2. New repository (top-right +) → owner techy → name Media-Server-Setup → NOT initialized (no README) → Create.
Your SSH key: Settings → SSH / GPG Keys → Add Key (paste ~/.ssh/id_ed25519.pub from dev).
G3. dev$ cd ~/claudevs/vanguard-stream && git remote add origin ssh://git@git.BASE:2222/techy/Media-Server-Setup.git && git push -u origin main
(accept the host key for port 2222) → "branch 'main' set up to track".
G4. dev$ ssh -t debian@HOST 'cd ~/vanguard-stream && ./registerGiteaRunner.sh'
→ log lines "runner: connect…" ; Site Administration → Actions → Runners lists vanguard-runner, Idle.
G5. Private window → https://git.BASE/user/login → "Sign in with authentik" → Discord → logged in as your Discord username.
(Requires git-users or git-admins membership; admins inherit git-admins via F5.)
## H. Jellyfin (browser at https://watch.BASE/)
H1. Wizard: language → admin user + password → Add Media Library:
Content type Shows · Display name Streams · Folder /media/streams · Metadata downloaders (Series, Season, Episode): tick ONLY "Vanguard Metadata" ·
Image fetchers: Series: Vanguard Metadata; Episode: Screen Grabber · Enable real time monitoring ✓ → OK.
Add Media Library: Shows · Clips · /media/clips · same provider settings → OK.
Add Media Library: Home Videos and Photos · Finals · /media/finals · all fetchers unticked → OK.
Finish the wizard (metadata language, remote access defaults).
H2. Dashboard → Playback → Transcoding → Hardware acceleration: None → Save.
H3. Dashboard → Plugins → My Plugins → Vanguard Metadata → settings: Discord bot token (A2), Server ID (A3), feed channel ID
(right-click the Discord channel → Copy Channel ID), Public URL https://watch.BASE, Authentik URL https://auth.BASE (token optional) → Save.
H4. Dashboard → Plugins → Repositories → + → Name SSO-Auth · URL https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/manifest-release/manifest.json → Save.
Catalog → SSO Authentication → Install. Then: dev$ ssh debian@HOST 'cd ~/vanguard-stream && docker compose restart jellyfin'
H5. Dashboard → Plugins → SSO-Auth → OpenID: Add provider: name authentik · OID Endpoint https://auth.BASE/application/o/jellyfin/ ·
Client ID / Secret from F10 · Enabled ✓ · Enable Authorization by Plugin ✓ · Roles claim: groups · Admin roles: admins ·
Enabled folders: Streams, Clips, Finals · Scheme Override: https · Port Override: EMPTY · (default: disable transcoding ✓) → Save (bottom of page).
H6. Private window → https://watch.BASE/sso/OID/start/authentik → Discord → lands in Jellyfin logged in.
H7. Dashboard → General → Branding → Login disclaimer: paste jellyfin/branding-disclaimer.html with watch.HOST replaced by watch.BASE;
Custom CSS: paste jellyfin/branding.css → Save. Login page now shows "Sign in with Discord" + a small "Admin login" link.
H8. Dashboard → Scheduled Tasks → Vanguard: run "snapshot Discord events", then "roll seasons", then "enforce playback policy" (▶ each).
Dashboard → Users → your SSO user → Playback → "Allow video playback that requires transcoding" is unticked.
## I. Pipeline
I1. dev$ ssh -t debian@HOST 'cd ~/vanguard-stream && ./switchToClone.sh key'
→ prints a public key. Gitea → repo → Settings → Deploy Keys → Add Deploy Key → paste → read-only → Add.
I2. dev$ ssh -t debian@HOST 'cd ~/vanguard-stream && ./switchToClone.sh clone && cd ~/vanguard-stream && ./configure.py --apply && sudo vanguard-deploy status'
→ "cloned. kept: …" ; status shows "main @ <commit>", DEPLOY_AUTO=0.
I3. dev$ ssh debian@HOST 'cd ~/vanguard-stream && docker network inspect vanguard-stream_default -f "{{(index .IPAM.Config 0).Gateway}}"; grep ^DEPLOY_WEBHOOK_SECRET= .env'
→ gateway IP (e.g. 172.18.0.1) and the secret.
I4. Gitea → repo → Settings → Webhooks → Add Webhook → Gitea: Target URL http://<gateway>:8081/deploy · HTTP POST · Content type JSON ·
Secret = the value from I3 · Trigger on Push events · Branch filter main · Active → Add Webhook. Open it → Test Delivery.
dev$ ssh debian@HOST 'journalctl -u dumpd --since -5m --no-pager | grep "deploy webhook"' → "… auto-deploy disabled (DEPLOY_AUTO=0)".
I5. From now on, after a push: dev$ ssh -t debian@HOST 'sudo vanguard-deploy pull' (shows the changed files and every action).
I6. (later, when trusted) dev$ ssh debian@HOST 'cd ~/vanguard-stream && sed -i "s/^DEPLOY_AUTO=.*/DEPLOY_AUTO=1/" .env && ./configure.py --apply >/dev/null && sudo vanguard-deploy status'
and dev$ cd ~/claudevs/vanguard-stream && git mv .gitea/draft-workflows/ci.yml .gitea/workflows/ci.yml && git commit -m "enable CI" && git push
## J. People
J1. Streamer (OBS): Tools → WebSocket Server Settings → Enable, set a password → OK.
Docks → Custom Browser Docks → name Vanguard, URL https://live.BASE/dock/ → Apply → log in with Discord inside the dock.
Dock → Settings: OBS WebSocket password → Save. Dock → Timecode band → "Add band + clock source" (while not streaming).
Dock: title → Go Live. Check: dev$ ssh debian@HOST 'journalctl -u dumpd --since -5m --no-pager | grep mtx-auth | tail -1' → "allow publish user=…".
J2. Gamer: https://clips.BASE/ → follow the three steps on the page (install Syncthing, paste Device ID, accept device + folder as Send Only).
Admin check: https://sync.BASE/ shows their device connected. Once: Syncthing → Actions → Settings → Connections → Incoming Rate Limit 12500 KiB/s.
J3. Editor: https://files.BASE/ (copyparty) → browse, download; https://files.BASE/finals/ to upload. Needs the editors role.
J4. Everyone: https://HOST/ (gateway page).
## K. Verify the whole pipeline once
K1. Stream 1 minute from OBS with the band, stop, wait ~8 min.
K2. dev$ ssh debian@HOST 'f=$(ls -t /srv/media/streams/*/*.mp4 | head -1); echo "$f"; ffprobe -v error -show_entries stream_tags=timecode -of compact=nk=0:p=0 "$f" | head -1; python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print({k:d.get(k) for k in (\"source\",\"timecode\",\"drift_ms\")}, \"gaps:\", d.get(\"gaps\",{}).get(\"count\"))" "${f%.*}.json"'
→ a flat file named <Title>__<timestamp>.mp4, tag:timecode set, source "strip", gaps 0.
K3. https://watch.BASE/ → the recording is an episode under this weekday's season; Discord feed channel got a post with a frame grab.
K4. https://live.BASE/ while someone streams → a window with the corner chip "webrtc".
## L. Discord bot (first release)
L1. Gitea → New Organization `VanguardOrg` → New Repository `VanguardBot` (private). The bot dev pushes to it.
L2. dev$ ssh debian@HOST 'cd ~/vanguard-stream && ./configure.py --show | grep -A2 "bot ci"'
→ copy BOT_DEPLOY_SECRET and the list of keys CI may set.
L3. Gitea → VanguardBot → Settings → Actions → Secrets → Add: `BOT_DEPLOY_SECRET` (L2), `DISCORD_BOT_TOKEN` (A2).
Variables → Add: `DISCORD_GUILD_ID` (A3). Optional `VANGUARD_DEPLOY_HOST` = the docker gateway from I3 if
the deploy job cannot detect it.
L4. Check the runner advertises the python312 label:
dev$ ssh debian@HOST 'cd ~/vanguard-stream && ./registerGiteaRunner.sh' (only if it does not)
Site Administration → Actions → Runners → vanguard-runner → labels include python312.
L5. Authentik → Applications → Create with provider → Proxy Provider, Forward auth (single application),
external host `https://bot.BASE` → add to the Embedded Outpost → bind group `bot-users`.
L6. The dev pushes to `main`. The run should end with the install log, "settings written: DISCORD_BOT_TOKEN,
DISCORD_GUILD_ID" and HTTP 200.
dev$ ssh debian@HOST 'cd ~/vanguard-stream && ./deployBotBundle.sh --status'
→ version, commit, package count, container Up.
L7. dev$ ssh debian@HOST 'docker logs discord-bot --tail 20'
→ the bot's version line, then its MariaDB connection. First real MariaDB run: watch for a retry loop.
L8. Private window → https://bot.BASE/ → Discord login → the bot's pages. Needs the `bot-users` group (admins
need it too, or set BOT_REQUIRE_GROUP= and let the bot's WEB_ALLOWED_GROUPS decide).
## M. Diagnostics (browser at https://diag.BASE/)
M1. dev$ ssh debian@HOST 'cd ~/vanguard-stream && docker compose ps --format "{{.Name}} {{.Status}}" | grep -E "elastic|kibana|beat"'
→ four Up, elasticsearch healthy. Elasticsearch restarting is almost always vm.max_map_count or the heap.
M2. https://diag.BASE/ → Discord login → Kibana. Discover shows logs within a minute of the Beats starting.
M3. Stack Management → Data Views → create one over `filebeat-*,metricbeat-*`, so a single filter spans logs
and metrics. Metricbeat also installed its own system and Docker dashboards: start from those.
M4. Stack Management → Index Lifecycle Policies → the beats policy → add a delete phase (30 days is sensible).
M5. Alerting → Connectors → Webhook → the URL from ALERT_WEBHOOK_URL. Then Rules → create the heartbeat rule:
always true, every minute, action = webhook POST to http://<docker gateway>:8081/heartbeat
(gateway: docker network inspect vanguard-stream_default -f '{{(index .IPAM.Config 0).Gateway}}').
M6. dev$ ssh debian@HOST 'sudo /usr/local/bin/vanguard-deadman.sh; echo exit=$?'
→ silent and exit 0 once M5 is done. Before that it correctly reports the heartbeat missing.
## N. Afterwards
N1. dev$ ssh debian@HOST 'docker inspect authentik-server --format "{{.Config.Image}}"' → put that tag in .env as AUTHENTIK_TAG, then ./configure.py --apply.
N2. Rotate anything pasted into a chat or screenshot (Discord client secret: portal → Reset → Authentik source → update).
N3. Backups: .env, /srv/appdata, /srv/media, /etc/vanguard/clip-quotas.json. Home archive puller: README §7.