install.sh, deploy.sh and the runbooks cover the bot bundle path

install.sh installs deployBotBundle.sh as vanguard-deploy-bot and creates /srv/bot/app; the pipeline
reinstalls it when it changes. FRESH-INSTALL gains section 7a and appendix L (the first release, the
python312 runner label, the Gitea secrets, the Authentik app), with the appendix relettered so it stays in
order. README's rebuild list says the bot's repo is outside this one and what to restore where.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-02 21:57:52 -06:00
parent ca10a9895f
commit 1ee8befdee
5 changed files with 52 additions and 6 deletions

View File

@@ -7,6 +7,8 @@ README.md (section numbers given).
## 0. Before touching the server ## 0. Before touching the server
- DNS: `A` record for `HOST` → the server IP; `CNAME`s `files`, `clips`, `sync`, `live`, `auth`, `watch`, `git`, `bot` → `HOST`. - DNS: `A` record for `HOST` → the server IP; `CNAME`s `files`, `clips`, `sync`, `live`, `auth`, `watch`, `git`, `bot` → `HOST`.
Optional: the bare domain `A` → server IP (it redirects to `https://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 developer portal → New Application (one app serves Authentik login, the Jellyfin plugin's bot and the
Discord bot): Discord bot):
- OAuth2 → Redirects: `https://auth.BASE/source/oauth/callback/discord/` (the last segment is the Authentik - OAuth2 → Redirects: `https://auth.BASE/source/oauth/callback/discord/` (the last segment is the Authentik
@@ -103,6 +105,18 @@ Log in at `https://git.BASE/` as `GITEA_ADMIN_USER` / `GITEA_ADMIN_PASSWORD` fro
(your SSH key under Settings → SSH keys). (your SSH key under Settings → SSH keys).
3. Runner: `./registerGiteaRunner.sh` on the server → Site Administration → Actions → Runners shows it idle. 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`.
## 8. Pipeline (README "Deployment") ## 8. Pipeline (README "Deployment")
1. On the server: `./switchToClone.sh key` → add the printed key as a read-only **Deploy Key** on the repo 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`. → `./switchToClone.sh clone` (keeps `.env`, generated configs, plugin zips) → `./configure.py --apply`.
@@ -305,8 +319,30 @@ K2. dev$ ssh debian@HOST 'f=$(ls -t /srv/media/streams/*/*.mp4 | head -1); echo
K3. https://watch.BASE/ → the recording is an episode under this weekday's season; Discord feed channel got a post with a frame grab. 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". K4. https://live.BASE/ while someone streams → a window with the corner chip "webrtc".
## L. Afterwards ## L. Discord bot (first release)
L1. dev$ ssh debian@HOST 'docker inspect authentik-server --format "{{.Config.Image}}"' → put that tag in .env as AUTHENTIK_TAG, then ./configure.py --apply. L1. Gitea → New Organization `VanguardOrg` → New Repository `VanguardBot` (private). The bot dev pushes to it.
L2. Rotate anything pasted into a chat or screenshot (Discord client secret: portal → Reset → Authentik source → update). L2. dev$ ssh debian@HOST 'cd ~/vanguard-stream && ./configure.py --show | grep -A2 "bot ci"'
L3. Backups: .env, /srv/appdata, /srv/media, /etc/vanguard/clip-quotas.json. Home archive puller: README §7. → 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. Afterwards
M1. dev$ ssh debian@HOST 'docker inspect authentik-server --format "{{.Config.Image}}"' → put that tag in .env as AUTHENTIK_TAG, then ./configure.py --apply.
M2. Rotate anything pasted into a chat or screenshot (Discord client secret: portal → Reset → Authentik source → update).
M3. Backups: .env, /srv/appdata, /srv/media, /etc/vanguard/clip-quotas.json. Home archive puller: README §7.

View File

@@ -45,6 +45,7 @@ Renaming a *label* (e.g. `watch` → `video`) means editing the first two; chang
| Gitea runner | registered with the instance URL → `./registerGiteaRunner.sh` again | git | | Gitea runner | registered with the instance URL → `./registerGiteaRunner.sh` again | git |
| Server clone | `git remote set-url origin ssh://git@GIT:2222/techy/Media-Server-Setup.git`; `~/.ssh/config.vanguard` Host line | git | | Server clone | `git remote set-url origin ssh://git@GIT:2222/techy/Media-Server-Setup.git`; `~/.ssh/config.vanguard` Host line | git |
| Dev box | `git remote set-url origin …` | git | | Dev box | `git remote set-url origin …` | git |
| Gitea, the bot repo | nothing for a rename; `VANGUARD_DEPLOY_HOST` only if it was set to an address | — |
| Streamers' OBS | dock URL (Custom Browser Docks), dock Settings → ingest host (re-save), clock browser source URL (dock → Timecode band → "Add band + clock source" again), any manual RTMP/SRT configs (copy from the dock) | live | | Streamers' OBS | dock URL (Custom Browser Docks), dock Settings → ingest host (re-save), clock browser source URL (dock → Timecode band → "Add band + clock source" again), any manual RTMP/SRT configs (copy from the dock) | live |
| Jellyfin apps on phones/TVs | server address | watch | | Jellyfin apps on phones/TVs | server address | watch |
| Bookmarks, Discord pinned links | main (gateway), clips, files, watch, live | all | | Bookmarks, Discord pinned links | main (gateway), clips, files, watch, live | all |

View File

@@ -497,7 +497,8 @@ secret) are gitignored and survive pulls; static pages are bind-mounted from the
| `configure.py`, `dock/`, `clips/`, `about/`, stignore | `configure.py --apply` → Caddy reload, copyparty restart | none / a second | | `configure.py`, `dock/`, `clips/`, `about/`, stignore | `configure.py --apply` → Caddy reload, copyparty restart | none / a second |
| `bin/*`, `systemd/*` | install; dumpd restart when it changed; timers pick up next run | a second | | `bin/*`, `systemd/*` | install; dumpd restart when it changed; timers pick up next run | a second |
| `firewall/nftables.conf` | `nft -f` | none | | `firewall/nftables.conf` | `nft -f` | none |
| `docker-compose.yml`, bot image | `compose up -d --build` for changed services | that service | | `docker-compose.yml`, `bot/Dockerfile`, `bot/entrypoint.sh` | `compose up -d --build` for changed services | that service |
| `deployBotBundle.sh` | reinstalled as `/usr/local/bin/vanguard-deploy-bot` (what dumpd runs for the bot's CI) | none |
| anything that changes `mediamtx.yml` or the mediamtx service | **restart only when no stream is live**; otherwise deferred, `deploy-pending.timer` retries every 5 min | none while live | | anything that changes `mediamtx.yml` or the mediamtx service | **restart only when no stream is live**; otherwise deferred, `deploy-pending.timer` retries every 5 min | none while live |
| `jellyfin-plugin-vanguard/dist/*.zip` (committed) | `installJellyfinPlugin.sh` | Jellyfin restart | | `jellyfin-plugin-vanguard/dist/*.zip` (committed) | `installJellyfinPlugin.sh` | Jellyfin restart |
| `live/` | `npm run build` on the server → `/srv/live` | none | | `live/` | `npm run build` on the server → `/srv/live` | none |
@@ -561,6 +562,10 @@ Still manual on a truly fresh box (not in the repo; documented in §5c–5g):
the domain-level proxy provider + outpost host, the Jellyfin and Gitea OIDC providers, application bindings the domain-level proxy provider + outpost host, the Jellyfin and Gitea OIDC providers, application bindings
- Jellyfin: first-run wizard, libraries (Shows + Vanguard provider), SSO plugin + provider, branding - Jellyfin: first-run wizard, libraries (Shows + Vanguard provider), SSO plugin + provider, branding
- Gitea: authentication source (authentik), deploy key, webhook; `./registerGiteaRunner.sh` - Gitea: authentication source (authentik), deploy key, webhook; `./registerGiteaRunner.sh`
- Discord bot: its repo lives outside this one, so nothing here restores it. Put `BOT_DEPLOY_SECRET`
(`./configure.py --show`), `DISCORD_BOT_TOKEN` and `DISCORD_GUILD_ID` into that repo's Gitea Actions
secrets/variables, then push to its `main` — or install a bundle by hand with `./deployBotBundle.sh`.
`/srv/bot/app` is created empty by `install.sh`; the container idles until a release lands there.
- Discord developer portal: application, redirect URIs, bot; DNS records - Discord developer portal: application, redirect URIs, bot; DNS records
A `.env`-driven Authentik blueprint would remove the first line (planned). A `.env`-driven Authentik blueprint would remove the first line (planned).
@@ -575,6 +580,8 @@ A `.env`-driven Authentik blueprint would remove the first line (planned).
/srv/media/clips/<user>/.stlocal Syncthing ignore list maintained by the sweep /srv/media/clips/<user>/.stlocal Syncthing ignore list maintained by the sweep
/srv/media/clips/<user>/.src/clip.mkv raw source, only for clips that did NOT arrive via Syncthing /srv/media/clips/<user>/.src/clip.mkv raw source, only for clips that did NOT arrive via Syncthing
/srv/media/clips/<user>/clip.normalize-failed.log only if normalization failed; delete to retry /srv/media/clips/<user>/clip.normalize-failed.log only if normalization failed; delete to retry
/srv/bot/app/ the Discord bot's installed release (bind-mounted at /app)
/srv/bot/previous/ the release before it (deployBotBundle.sh --rollback)
/srv/media/finals/ editors upload here /srv/media/finals/ editors upload here
/srv/media/trash/<night>/... dumped, purged after 3 days /srv/media/trash/<night>/... dumped, purged after 3 days
/srv/media/.markers/ dump markers /srv/media/.markers/ dump markers

View File

@@ -49,6 +49,7 @@ apply_changes() {
if has '^bin/'; then run install -m 0755 bin/normalize-sweep.sh bin/trash-purge.sh bin/dumpd.py bin/vgstrip.py bin/vgdeband.sh /usr/local/bin/; fi if has '^bin/'; then run install -m 0755 bin/normalize-sweep.sh bin/trash-purge.sh bin/dumpd.py bin/vgstrip.py bin/vgdeband.sh /usr/local/bin/; fi
if has '^systemd/'; then run install -m 0644 systemd/*.service systemd/*.timer /etc/systemd/system/; run systemctl daemon-reload; fi if has '^systemd/'; then run install -m 0644 systemd/*.service systemd/*.timer /etc/systemd/system/; run systemctl daemon-reload; fi
if has '^deploy\.sh$'; then run install -m 0755 deploy.sh /usr/local/bin/vanguard-deploy; fi if has '^deploy\.sh$'; then run install -m 0755 deploy.sh /usr/local/bin/vanguard-deploy; fi
if has '^deployBotBundle\.sh$'; then run install -m 0755 deployBotBundle.sh /usr/local/bin/vanguard-deploy-bot; fi
if has '^firewall/nftables\.conf$'; then run install -m 0644 firewall/nftables.conf /etc/nftables.conf; run nft -f /etc/nftables.conf; fi if has '^firewall/nftables\.conf$'; then run install -m 0644 firewall/nftables.conf /etc/nftables.conf; run nft -f /etc/nftables.conf; fi
# 2. regenerate configs (Caddy reload, copyparty restart) — configure.py decides; MediaMTX is handled below # 2. regenerate configs (Caddy reload, copyparty restart) — configure.py decides; MediaMTX is handled below

View File

@@ -41,7 +41,7 @@ install -d -o 1000 -g 1000 /srv/media/{streams,clips,finals,trash,.markers,.sync
install -d -o 1000 -g 1000 /srv/appdata/jellyfin/{config,cache} /srv/appdata/authentik/{media,templates,certs} install -d -o 1000 -g 1000 /srv/appdata/jellyfin/{config,cache} /srv/appdata/authentik/{media,templates,certs}
install -d /srv/appdata/authentik/{db,redis} /srv/appdata/mariadb install -d /srv/appdata/authentik/{db,redis} /srv/appdata/mariadb
install -d -o 1000 -g 1000 /srv/appdata/discord-bot /srv/appdata/gitea /srv/appdata/act_runner install -d -o 1000 -g 1000 /srv/appdata/discord-bot /srv/appdata/gitea /srv/appdata/act_runner
install -d -o 1000 -g 1000 /srv/bot /srv/bot/app # the Discord bot's deployed CI bundle (bind-mounted at /app) install -d -o 1000 -g 1000 /srv/bot /srv/bot/app # the Discord bot's released CI bundle (bind-mounted at /app; see bot/README.md)
install -d -o "$OWNER" /srv/live # live portal static build (deployLive.sh) install -d -o "$OWNER" /srv/live # live portal static build (deployLive.sh)
install -d -o 1000 -g 1000 /srv/media/streams/{techy,diceofire} /srv/media/clips/{techy,diceofire} install -d -o 1000 -g 1000 /srv/media/streams/{techy,diceofire} /srv/media/clips/{techy,diceofire}
for d in /srv/media/clips/*/; do for d in /srv/media/clips/*/; do
@@ -51,6 +51,7 @@ done
echo "== host scripts + units" echo "== host scripts + units"
install -m 0755 bin/normalize-sweep.sh bin/trash-purge.sh bin/dumpd.py bin/vgstrip.py bin/vgdeband.sh /usr/local/bin/ install -m 0755 bin/normalize-sweep.sh bin/trash-purge.sh bin/dumpd.py bin/vgstrip.py bin/vgdeband.sh /usr/local/bin/
install -m 0755 deployBotBundle.sh /usr/local/bin/vanguard-deploy-bot # dumpd runs this for the bot CI's POST /deploy-bot
install -d -m 0750 /etc/vanguard install -d -m 0750 /etc/vanguard
install -m 0640 dump-users.txt /etc/vanguard/dump-users.txt install -m 0640 dump-users.txt /etc/vanguard/dump-users.txt
install -m 0644 systemd/*.service systemd/*.timer /etc/systemd/system/ install -m 0644 systemd/*.service systemd/*.timer /etc/systemd/system/