Media Relayout.md hit the vault's 500-line gate the moment the tvdb contract landed, and the fat in it was not the new text: the Plex and Jellyfin scan blocks (35 lines) sat inside the relayout procedure even though they are the way you talk to those servers after ANY change, and two Laws bullets repeated the same facts a third time. Both blocks now live on [[Plex]] and [[Jellyfin]]; the relayout page keeps the two laws an operator needs at that moment — 204 is acceptance and anything else queued nothing, and one accepted refresh is the whole procedure, because a second moves `Scan Media Library` to `Cancelling` and every later 204 is discarded. Same pass, the dead numbers the vault's own law says to destroy: an applied music pass's date, an applied audiobook pass's move/park counts, and the 146 sweep stragglers of one specific first sweep. The laws they taught stay. 490 lines, `bats tests/docs_completeness.bats` 6/6 (length violation gone, link integrity clean).
117 lines
6.3 KiB
Markdown
117 lines
6.3 KiB
Markdown
---
|
|
type: reference
|
|
description: "Plex Media Server is a **containerized media server on Dankserver**: a podman"
|
|
tags:
|
|
- dankserver
|
|
- plex
|
|
timestamp: 2026-10-30
|
|
---
|
|
|
|
# Plex
|
|
|
|
Plex Media Server is a **containerized media server on Dankserver**: a podman
|
|
quadlet (`plex.service`) under the systemd **user** generator, generated from
|
|
`dankserver/plex/quadlet/plex.container`. It shares the host with a second live
|
|
media server, [[Jellyfin]] (`jellyfin.service`, listening on 8096); both serve
|
|
from the same tree — the [[Media Metadata View]] overlay — so the Kodi-format
|
|
sidecars Jellyfin writes are the files Plex reads, with no copy and no sync.
|
|
|
|
**Host:** Dankserver — **Ports:** 32400/tcp (PMS web + API), 32400 is the
|
|
only port PMS needs for client streaming; LAN clients connect directly,
|
|
public traffic can be fronted by Caddy like every other vhost.
|
|
|
|
## Facts
|
|
|
|
| Item | Value |
|
|
|------|-------|
|
|
| Runtime | podman quadlet, `plex.service` (user manager) — migrated from the native `plexmediaserver.service` OS-package unit (2026-09) |
|
|
| Image | `lscr.io/linuxserver/plex:1.43.5.11029-6bf8af3f0-ls329` (pinned; verified == the registry's `latest` manifest digest at pin time: `sha256:8f655bad…e9e2334`) |
|
|
| Data root | `/var/lib/plexmediaserver` bind-mounted **in place** as `/config` (RW) — the migration moved zero bytes |
|
|
| Media | git: `/var/lib/media-meta/view` (the [[Media Metadata View]] overlay) bound at the same absolute path `/mnt/entropy` **read-only**; live today: `/mnt/entropy` itself, read-only — the view has never been installed. Either way every library path in the PMS database stays valid, zero re-scan |
|
|
| Identity | fleet rootless model — container root maps to the `dank` user (1000:1000); the deployer chowns the data root on first cutover |
|
|
| Port | `32400/tcp` published on `0.0.0.0` — zero client reconfiguration |
|
|
| Network | `dankserver_appnet`, alias `plex`; Tautulli reaches it at `http://plex:32400` |
|
|
| GPU | none by decision — CPU decode only; the two R9700s stay dedicated to [[frank-archon]] |
|
|
| Deploy | `./scripts/deploy.sh dankserver --service plex` |
|
|
| Docs | https://support.plex.tv |
|
|
|
|
## Why in-place bind mounts
|
|
|
|
The migration moved zero bytes: the container bind-mounts the native data
|
|
root where it already lived, and the media archive re-mounts at
|
|
`/mnt/entropy` inside the container — only the HOST side of that bind has moved
|
|
(the array today; the overlay view in git has never been deployed), so every
|
|
absolute library path in the
|
|
PMS database keeps resolving. The one metadata change: rootless podman
|
|
cannot map the real host uid 997, so the fleet model applies — container
|
|
root runs as the `dank` user, and the deployer adopts the data root with
|
|
`chown -R 1000:1000` on first cutover (rollback chowns it back).
|
|
|
|
## Cutover & rollback
|
|
|
|
- First deploy stops and **masks** `plexmediaserver.service` (never
|
|
`disable` — on these hosts `disable` deletes the unit file), chowns
|
|
`/var/lib/plexmediaserver` to `1000:1000`, then starts the quadlet; the
|
|
deployer blocks until `/identity` answers on 32400.
|
|
- Rollback lever while masked: `systemctl --user stop plex.service && sudo
|
|
chown -R 997:984 /var/lib/plexmediaserver && sudo systemctl unmask
|
|
plexmediaserver.service && sudo systemctl start plexmediaserver.service`
|
|
(ownership-only churn — zero bytes ever moved).
|
|
- Decommission (`sudo apt-get -y purge plexmediaserver`) only after explicit
|
|
operator sign-off.
|
|
|
|
## Integration points
|
|
|
|
- **Caddy:** Plex is exempt from the Anubis bot challenge
|
|
(plain-HTTP legacy clients cannot solve JS challenges) —
|
|
[[Anubis]] "Exempt by construction".
|
|
- **Tautulli** ([[Tautulli]]) is the companion: it polls PMS on the appnet
|
|
for statistics and notifications and makes no sense without Plex.
|
|
- **Auth wall:** if a public vhost is ever pointed at Plex, put the
|
|
Authentik outpost + Anubis chain in front of it like every other guarded
|
|
vhost ([[Authentik]], [[Caddy]]); direct `:32400` exposure to the LAN is
|
|
unauthenticated by design (Plex owns its own auth via claim tokens).
|
|
|
|
## Gotchas
|
|
|
|
1. **Do not benchmark on it blind.** Transcoding is CPU-only now but still
|
|
contends with the LLM fleet's host-RAM expert streaming; stop the service
|
|
before benchmark runs, per the Benchmarking Policy.
|
|
2. **Updates are commit-driven, not apt.** The OS package is retired at
|
|
cutover; upgrading PMS means pinning a new `-ls` tag in the unit file and
|
|
redeploying — never run `:latest`.
|
|
3. The claim token is the identity of this server; document it in the host
|
|
`.env`, never in this repo (secret rules apply). The token lives in
|
|
`Preferences.xml` inside the RW data bind, preserved by the migration.
|
|
4. **Cold container boot is slow.** PMS pre-binds 32400 and empty-replies
|
|
while the HTTP layer finishes loading; the first boot of a years-old
|
|
library took ~10 minutes (2026-09 cutover). The deployer gate now allows
|
|
600 s before failing; later boots are much faster (warm page cache).
|
|
|
|
## Triggering a scan
|
|
|
|
A rename on disk is invisible until the section is rescanned, and a moved file is
|
|
a `Couldn't find the file to stream` 404 until that scan passes it. Run this from
|
|
the host shell through `podman exec`, never from a neighbour container: PMS's
|
|
HTTPD closes without a single byte on any request that arrives from another
|
|
container's netns (10.89.x.x reads as "empty reply", not as a 401). The token
|
|
rides stdin, never argv, where any process on the host could read it out of
|
|
`/proc/<pid>/cmdline`; it lives in `Preferences.xml` inside the RW data bind.
|
|
|
|
```bash
|
|
umask 077
|
|
podman exec plex sh -c "sed -n 's/.*PlexOnlineToken=\"\\([^\"]*\\)\".*/\\1/p' \
|
|
'/config/Library/Application Support/Plex Media Server/Preferences.xml'" >/tmp/px.tok
|
|
podman exec -i plex sh -c 'read -r T
|
|
for k in $(curl -s -H "X-Plex-Token: $T" -H "Accept: application/json" \
|
|
http://127.0.0.1:32400/library/sections | grep -o "\"key\":\"[0-9]*\"" | cut -d\" -f4); do
|
|
curl -s -o /dev/null -w "plex section $k: %{http_code}\n" -X POST \
|
|
-H "X-Plex-Token: $T" "http://127.0.0.1:32400/library/sections/$k/refresh"
|
|
done' </tmp/px.tok
|
|
```
|
|
|
|
Every section answers `204` on success. Proof that a rename landed is not a scan
|
|
response — `scripts/media-verify.py` replays the run's journal against the
|
|
database, which is the only read that distinguishes "not scanned yet" from "gone";
|
|
see [[Media Relayout]].
|