Files
Media-Server-Setup/HOSTNAME-CHANGE.md
2026-11-06 19:05:47 -06:00

239 lines
20 KiB
Markdown

# Changing hostnames safely
Two cases: renaming one subdomain (e.g. `watch.BASE` → `video.BASE`), and moving the whole stack to a new base
domain. Both are mostly `configure.py --full` plus a known list of places outside the repo that hold the name.
Do it between streams: the main host is MediaMTX's WebRTC advertised host, so a main-host change needs a
MediaMTX restart.
Roles: `main` (gateway page at `/`, `/onboarding/`), `files` (copyparty), `clips`, `sync`, `live` (portal + Go Live,
`/dock/`, `/dock/clock/`, `/dump`, `/me`, `/time`, `/whoami`), `auth`, `watch`, `git`, `bot`, `diag` (Kibana at `/`,
`/streaming`, `/disk`, dumpd's `/streaming/api/*`), and optionally `apex` (the bare domain, a redirect to
`APEX_REDIRECT`; it needs its own A record at the box or Caddy cannot get its certificate — as of 2026-11-06 the bare
`dumpstervigilantes.rip` resolves elsewhere, so leave `APEX_HOST` empty or fix the record first). Ours: `media.`,
`files.`, `clips.`, `sync.`, `live.`, `diag.`, … under `dumpstervigilantes.rip`. `ovh1.BASE` is ssh only.
## The one rule: apps before Authentik
Change the app, then Authentik, never the other way round. An app that already lives on its new name but whose
Authentik provider still says the old one is simply unreachable until Authentik is updated; a provider pointed at
a name the app does not answer on yet locks you out of the app, and if that app is the gate in front of the admin
UI, out of everything. So in every procedure below Authentik comes after the servers, the containers, Jellyfin and
Gitea — and the Discord developer portal after Authentik. The safety net at every step is akadmin's password
card, `https://AUTH/if/flow/default-authentication-flow/?admin=1`, which bypasses the Discord forward.
Note that `configure.py --apply` does touch Authentik on its own: the login redirect stage's target (the Discord
source's login URL on the auth host) and the brand's default application. Both are Authentik-internal, neither
decides whether an app is reachable, and the admin card ignores the redirect stage — so they cannot lock you out.
The things that can — outpost host, proxy provider external host and cookie domain, Jellyfin/Gitea redirect
URIs — are never changed by any script here.
## Where hostnames live
### Generated on the server by `configure.py` (regenerated by `--full`, nothing to edit by hand)
| Place | Holds |
|---|---|
| `Caddyfile` | every site block — the source of truth `configure.py` reads back (`# vanguard:<role>` markers) |
| `.env` | `MAIN_HOST`, `FILES_HOST`, `CLIPS_HOST`, `SYNC_HOST`, `DIAG_HOST`, `WATCH_HOST`, `GIT_HOST`, `BOT_HOST`, `LIVE_HOST`, `APEX_HOST`, `APEX_REDIRECT`; during a move `ALT_BASE` (every site also answers under it) and `REDIRECT_BASE` (old names 308 to the new) |
| `mediamtx.yml` | `webrtcAdditionalHosts: [MAIN_HOST]` (needs a MediaMTX restart to take effect) |
| `docker-compose.yml` → container env | Jellyfin `PublishedServerUrl` (WATCH), Gitea `DOMAIN/ROOT_URL/SSH_DOMAIN` (GIT), bot `WEB_PUBLIC_URL` (BOT), MediaMTX `LIVE_HOST` (text of the "is live" post), act_runner `GITEA_INSTANCE_URL` (GIT), Kibana `server.publicBaseUrl` via `DIAG_HOST` (DIAG). `compose up -d` in the apply recreates the affected containers (seconds each). MediaMTX is on the host network and is deliberately **not** recreated by the apply |
| Authentik, by `configure.py --apply` (`--ak-branding` alone) | the login **redirect stage** target `https://AUTH/source/oauth/login/<discord-slug>/` (members never see an Authentik page), the brand's default application (the proxy provider whose external host is MAIN), logo/favicon/background paths (relative, no host). Re-run after an **auth** or **main** change |
| Kibana landing dashboard, by `configure.py --apply` (`KEY=… kibana-setup.py landing` alone) | the Vanguard tile's links: LIVE, FILES, WATCH, MAIN, GIT, BOT, SYNC from `.env`, AUTH derived as `auth.<domain of MAIN>`; `/streaming`, `/disk` relative. Re-run after any change |
| Caddy TLS | certificates for new names are fetched automatically; old ones linger harmlessly |
### Pages in the repo that derive sibling hosts from the browser's location (label names hard-coded)
| File | Assumes |
|---|---|
| `about/index.html` | `files.`, `clips.`, `sync.`, `live.`, `auth.`, `watch.`, `git.`, `bot.`, `diag.` + the parent of the current host (`sub('<label>')`); `/onboarding/` relative |
| `dock/index.html` | `auth.` (sign-out), ingest host prefilled from the current host (`live.`; any name of the box works for RTMP/SRT); `/dump`, `/me/*`, `/time` relative |
| `live/` portal, `streaming/index.html`, `streaming/disk.html`, `about/onboarding/` | nothing: every call is same-origin (`/webrtc/`, `/hls/`, `/me/*`, `/streaming/api/*`) |
| `switchToClone.sh`, `deployLive.sh` | defaults for `git.dumpstervigilantes.rip` / `media.dumpstervigilantes.rip` (override by env/arg, or edit) |
| `README.md`, `FRESH-INSTALL.md` | examples only |
Renaming a *label* (e.g. `watch` → `video`) means editing the first two; changing only the domain does not.
### Outside the repo — must be changed by hand
| System | Setting | Which host |
|---|---|---|
| DNS | A record + CNAMEs | all |
| Discord developer portal | OAuth2 redirect `https://AUTH/source/oauth/callback/discord/` | auth |
| Authentik | Embedded Outpost `authentik_host` | auth |
| Authentik | `Vanguard sites` proxy provider: authentication URL (auth) and **cookie domain** (base) | auth, base |
| Authentik | `Jellyfin` provider redirect URI `https://WATCH/sso/OID/redirect/authentik` | watch |
| Authentik | `Gitea` provider redirect URI `https://GIT/user/oauth2/authentik/callback` | git |
| Jellyfin | SSO-Auth plugin: OID endpoint `https://AUTH/application/o/jellyfin/` | auth |
| Jellyfin | Branding login disclaimer (the form action contains the watch host) | watch |
| Jellyfin | Vanguard Metadata plugin: Public URL, Authentik URL | watch, auth |
| Gitea | Authentication source `authentik`: discovery URL `https://AUTH/application/o/gitea/.well-known/openid-configuration` | auth |
| 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 |
| 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 |
| Jellyfin apps on phones/TVs | server address | watch |
| Bookmarks, Discord pinned links | main (gateway), clips, files, watch, live | all |
Not affected: Syncthing pairings (device IDs), MariaDB, stream tokens, the Gitea webhook (gateway IP), Authentik
users/groups/policies, the alert pipeline (Discord webhook + loopback Elasticsearch), the deadman, the publish block
list and the WebRTC session history (`.markers/`), MediaMTX's host-network ports.
## A. Renaming one subdomain (example: `watch.BASE` → `video.BASE`)
1. DNS: add `CNAME video.BASE → HOST`. Keep the old record until step 7.
2. Server, between streams: `./configure.py --full` → Enter keeps every value; type the new name at the
"Jellyfin hostname" prompt; `y`. Caddy reloads (new block, old gone), Jellyfin is recreated with the new
published URL. `curl -sI https://video.BASE/ | head -1` → 200/302 once the certificate is issued.
(Only the *main* host touches `mediamtx.yml`; other hosts need no MediaMTX restart.)
3. Jellyfin first: Dashboard → General → Branding → disclaimer form action → new host → Save.
Dashboard → Plugins → Vanguard Metadata → Public URL → new host → Save. (`https://video.BASE/` now loads and
fails only at the SSO step: expected until step 4.)
4. Authentik last: Applications → Providers → `Jellyfin` → Edit → redirect URI → the new host → Update.
5. If the *label* changed (`watch`→`video`), edit `about/index.html` (`sub('watch')` → `sub('video')`) — commit, push,
`sudo vanguard-deploy pull`. (`.env APEX_REDIRECT` too if the bare domain pointed at it: `./configure.py --full`.)
6. Private window: `https://video.BASE/` → Discord → in. The gateway `https://HOST/` links to the new name.
7. Remove the old DNS record. Tell members with Jellyfin apps to update the server address.
Per-host extras when it is a different host:
- **auth**, in this order: Jellyfin SSO plugin OID endpoint and Vanguard plugin Authentik URL; Gitea auth source
discovery URL; `dock/index.html` + `about/index.html` if the label changed; THEN Authentik's outpost
`authentik_host` and the proxy provider's authentication URL (log in there with the admin card); THEN the
Discord portal redirect. Everyone re-logs in.
- **git**: Authentik Gitea provider redirect; `./registerGiteaRunner.sh`; server clone remote + `~/.ssh/config.vanguard`;
dev remote; `switchToClone.sh` default.
- **live**, **bot**: only the compose env (automatic) and `about/index.html` if the label changed. Streamers who use
Go Live need nothing (same-origin); OBS users re-add the dock URL (the dock lives on `live`).
- **diag**: compose env → Kibana's `publicBaseUrl` (automatic, Kibana recreated), the Kibana landing tile
(automatic in the apply), `DIAG_REQUIRE_GROUP` in `.env` if the gate group should differ, `about/index.html`
if the label changed. Alarm rules and the Discord alert bus carry no hostname.
- **auth**, in addition to the list above: `configure.py --ak-branding` (the apply does it) so the Discord-forward
redirect stage points at the new auth host — until then every login lands on the old name; the admin card
moves to `https://NEW-AUTH/if/flow/default-authentication-flow/?admin=1` (the Kibana tile link follows).
- **main (HOST)**: `mediamtx.yml` changes → `docker compose restart mediamtx` between streams; every streamer
re-adds the dock URL, re-saves Settings (ingest host), and re-runs "Add band + clock source"; manual RTMP/SRT
configs are re-copied from the dock; `deployLive.sh` default host.
## B. Changing the base domain (everything at once)
No maintenance window: every site answers on **both** bases first (`--alias`), so the old names keep working for
members while the apps and Authentik are moved one by one; the flip (`--base`) is the last server step and turns the
old names into redirects. Order follows the rule above: servers, then the apps, then Authentik, then Discord, then
the flip. `OLD` = the current base, `NEW` = the new one. Members' sessions survive until step 6; from step 6 on
they log in once on the new names.
1. **DNS on NEW** (nothing else until this resolves): `A media.NEW` or CNAMEs for `media`, `files`, `clips`, `sync`,
`live`, `diag`, `auth`, `watch`, `git`, `bot` → the box. Leave every OLD record in place. Verify:
`for h in media files clips sync live diag auth watch git bot; do printf '%-6s ' $h; getent hosts $h.NEW | awk '{print $1}' | tail -1; done`
— ten identical addresses. No bare-domain record unless it really points at the box.
2. **Both bases live:** `./configure.py --alias NEW`. Only the Caddyfile changes (`media.OLD, media.NEW {` …) and
Caddy reloads; certificates for the new names follow within a minute. Nothing else moves. Check:
`curl -sI https://auth.NEW/ | head -1` → 302, and the old names unchanged. `sudo vghosts-check.py` now also
probes every alias.
3. **Discord developer portal** → OAuth2 → Redirects → **add** `https://auth.NEW/source/oauth/callback/discord/`
beside the old one.
4. **Jellyfin** (any name, SSO still works): SSO-Auth `authentik` provider OID endpoint
`https://auth.NEW/application/o/jellyfin/` — works at once, Authentik answers on both names; Branding login
disclaimer `https://watch.NEW/sso/OID/start/authentik`; Vanguard Metadata Public URL `https://watch.NEW`,
Authentik URL `https://auth.NEW`.
5. **Gitea**: Site administration → Authentication sources → `authentik` → discovery URL
`https://auth.NEW/application/o/gitea/.well-known/openid-configuration` (works at once, same reason).
Server clone remote and `~/.ssh/config.vanguard` → `git.NEW` (the host answers already). Dev box remote likewise.
6. **Authentik, then the flip, back to back, between streams.** This is the only moment anything is unreachable,
and it is seconds:
`./configure.py --ak-hosts OLD NEW --dry-run` (read it) → `./configure.py --ak-hosts OLD NEW` → immediately
`./configure.py --base NEW` → `docker compose restart mediamtx` → `./configure.py --ak-branding` →
`./registerGiteaRunner.sh`.
`--ak-hosts` moves the outpost host, the proxy provider (external host + cookie domain `NEW`) and the
Jellyfin/Gitea redirect URIs: from this instant a login only sticks on the NEW names. `--base` makes NEW the
primary everywhere (`.env`, `mediamtx.yml`, Jellyfin/Gitea/runner/bot/Kibana recreated, Kibana tile), drops
the alias, and gives every OLD name a block that 308s to its NEW name, so bookmarks and pinned links keep
working. Members on an old name get redirected and log in once.
7. **Test, private window:** `https://auth.NEW/` → Discord → in. `https://media.OLD/` → 308 → `https://media.NEW/`
→ gate → in. `https://watch.NEW/` SSO round trip. `https://git.NEW/user/login` authentik sign-in.
`sudo vanguard-deploy pull`. `sudo vghosts-check.py` → 0 failed (it also checks every OLD name 308s).
8. **Repo defaults:** `deployLive.sh` (HOST), `switchToClone.sh` (REPO_SSH) → NEW; commit, push, pull.
9. **People:** OBS streamers re-add the dock at `https://live.NEW/dock/` (the old URL redirects, but OBS docks
want the real one), re-save Settings, "Add band + clock source" again; Go Live users just reload. Jellyfin apps
→ new server address. Pins can stay, they redirect; re-pin at leisure. Remove the OLD Discord redirect entry
once everyone has logged in. The OLD DNS records stay as long as the NEW names are CNAMEs onto them.
Rollback before step 6: `./configure.py --alias ""` and revert the two Jellyfin/Gitea screens. Rollback after step 6:
`./configure.py --ak-hosts NEW OLD` then `./configure.py --base OLD` (which makes the NEW names the redirects).
## C. Splitting the single main host into media / files / clips / sync (done 2026-11-01)
Before: one main host (`ovh1.BASE`) served copyparty at `/`, `/about/`, `/clips/`, `/sync/`, `/dock/`, `/dump`, `/time`.
After: `media.BASE` = gateway at `/`; `files.BASE` = copyparty; `clips.BASE`; `sync.BASE`; `live.BASE` gains `/dock/`,
`/dock/clock/`, `/dump`, `/me`, `/time`, `/whoami`. `/time` is only on `live` because the two pages that fetch it
(the clock source and Go Live) are same-origin there. Authentik needs nothing: the domain-level provider covers every
name under BASE, the outpost host (`auth.`) is unchanged.
1. DNS: CNAMEs `media`, `files`, `clips`, `sync` → the box (keep `ovh1` for ssh; it is no longer a web host).
2. Dev box: pull/upload the repo (`configure.py`, `about/`, `addCommunityMediaServer.sh`, docs).
3. Server, between streams: `./configure.py --full` → main hostname `media.BASE`; Enter for the sub-hostnames
(defaults `files.`, `clips.`, `sync.`, …); bare domain redirect → `https://media.BASE/`; `y`.
Then `docker compose restart mediamtx` (webrtcAdditionalHosts is now `media.`) and `./deployLive.sh` (portal
build picks up nothing new, but confirms `/srv/live` is served on `live.`).
4. Streamers: OBS → Custom Browser Docks → replace the dock URL with `https://live.BASE/dock/`; dock Settings →
save; Timecode band → "Add band + clock source" again (the clock browser source URL moved).
5. Discord: re-pin `https://media.BASE/` (members), `https://clips.BASE/` (gamers), `https://files.BASE/` (editors).
6. Old `ovh1` bookmarks stop working (no Caddy block, TLS fails) — point people at the gateway.
## D. Adding a NEW host
**`./configure.py --apply` cannot do this, and will not complain.** It is stateless by design: it reads the
hostnames back out of the `Caddyfile` (`# vanguard:<role>` markers), so a role that is not in there yet has no
value to write and the site is simply left out. The containers come up, Caddy reloads, and the new name has no
site block and therefore no certificate — which presents as an SSL handshake error, not as an error from the
apply. This cost an hour on 2026-11-03 when `diag.` was added.
Introduce a new host with **`--full`** (asks for every hostname, defaults from the current config) or with
**`--hosts`**, which is the non-interactive form and takes them positionally:
./configure.py --hosts AUTH WATCH GIT BOT LIVE FILES SYNC CLIPS DIAG # "" leaves one unset
After that, `--apply` maintains it like any other host, because the marker is now in the file.
1. DNS: `CNAME` (or `A`) for the new name → the box; check it resolves before continuing.
2. Add the role to `configure.py`: a `write_caddy` block, a `<ROLE>_REQUIRE_GROUP` default in `write_env`,
the prompt in `full()`, the positional in `--hosts`, and the line in `show()`.
3. `./configure.py --hosts …` with every hostname including the new one.
4. Verify the marker landed: `grep -c "vanguard:<role>" Caddyfile` → 1. Then `curl -sI https://NEW/` → 302.
5. Add it to `addCommunityMediaServer.sh` so a rebuild asks for it too.
## E. Validating a change: `sudo vghosts-check.py`
One command on the server checks every place in the tables above against the Caddyfile, the same source of
truth `configure.py` reads back, and prints `ok` / `FAIL` / `warn` per item plus a `todo` list for the things no
script can read (the Discord developer portal, streamers' OBS). Exit code 1 if anything failed.
server$ cd ~/vanguard-stream && sudo vghosts-check.py # everything
dev$ ./bin/vghosts-check.py --repo # repo defaults, about-page labels, docs only
server$ sudo vghosts-check.py --json # for a script
**The order is the order of the change, and it matters.** The sections run top-down because each one depends
on the one above it, so a failure early on is usually the *cause* of the failures below it; fix the first
failure and re-run rather than chasing the later ones:
| # | Section | Depends on | Why this position |
|---|---|---|---|
| 1 | DNS | — | nothing gets a certificate or a visitor until the name resolves to the box |
| 2 | Caddyfile | 1 | the roles and names `--full`/`--hosts` wrote; Caddy fetches certificates from here |
| 3 | `.env` | 2 | the same names as compose variables; a mismatch is a half-finished `--full` |
| 4 | `mediamtx.yml` | 3 | `webrtcAdditionalHosts` = main host; only a MediaMTX restart (between streams) applies it |
| 5 | containers | 3 | the env each container was *created* with: Jellyfin, Gitea, runner, bot, MediaMTX, Kibana |
| 6 | HTTPS | 1, 2 | every name answers with a valid certificate; gated names bounce to the auth host |
| 7 | Jellyfin | 5, 6 | SSO plugin OID endpoint, branding disclaimer, Vanguard plugin URLs; live SSO start |
| 8 | Gitea | 5, 6 | `app.ini`, the auth source (live redirect), the runner's registration, the server clone's remote |
| 9 | Authentik | 7, 8 | **last of the services**: outpost host, proxy provider external host + cookie domain, Jellyfin/Gitea redirect URIs — changing these before the apps is the one way to lock yourself out |
| 10 | Discord portal | 9 | the OAuth2 redirect is printed exactly; Discord login is broken until it matches; akadmin's card still works |
| 11 | repo | 2 | `deployLive.sh`/`switchToClone.sh` defaults, about-page labels = Caddy roles, no browser links to `ovh1` in the docs |
| 12 | streamers | 2 | OBS dock URL, clock source, Jellyfin apps, Discord pins: listed, never checked |
What it knows that a human forgets: a container keeps the environment it was created with, so step 5 catches a
`.env` that changed without `docker compose up -d <service>`; Authentik's cookie domain decides whether one
login spans every host; and `ovh1.BASE` must resolve (ssh) but must *not* answer https.
By hand, for the round trips the script only probes:
https://HOST/whoami → your identity (gate works on the new name)
https://watch.BASE/sso/OID/start/authentik → Jellyfin SSO round trip
https://git.BASE/user/login → "Sign in with authentik" round trip
dock → Go Live → journalctl -u dumpd | grep mtx-auth → allow publish
live.BASE → a window appears for the test stream, chip "webrtc"