The merged door was /mnt/media, which read like a third volume and split the host state across two trees. It is now /var/lib/media-meta/view, so the array layer, the SSD upper layer, the kernel workdir, the reap trash and the merged tree are one directory — and the mount table gains no new /mnt entry beside the array. systemd names a mount unit after its ESCAPED path, so the repo copy keeps the readable name media-meta-view.mount while `install` writes the host file as var-lib-media\x2dmeta-view.mount; `plan` prints both and the page records them, so nobody hunts for a unit that "does not exist". install also creates the mount point itself, because systemd will not. The seam now additionally pins that the unit's Where= is exactly the path both containers bind — guard and bind cannot drift apart. The Quadlet Lifecycle phantom-path example is corrected too: it named an invented /mnt/media/*, while the real phantom is the legacy /opt/jellyfin/* that dankserver/compose.yaml still carries for a service whose roots moved to /home/dank/Containers/jellyfin.
131 lines
6.7 KiB
Markdown
131 lines
6.7 KiB
Markdown
---
|
|
type: reference
|
|
title: Jellyfin
|
|
description: "Jellyfin on Dankserver: quadlet unit, overlay metadata view layout, ports, access, hardware-transcode status."
|
|
tags: [jellyfin, dankserver, media-server]
|
|
timestamp: 2026-10-30T00:00:00Z
|
|
---
|
|
|
|
# Jellyfin
|
|
|
|
Jellyfin is the free media server on Dankserver — a podman quadlet service
|
|
(`jellyfin.service`, generated from `dankserver/jellyfin/quadlet/jellyfin.container`)
|
|
serving music, movies, and shows over LAN. It replaced the earlier research
|
|
brief; it runs alongside [[Plex]], which remains the primary server for some
|
|
clients.
|
|
|
|
**Host:** Dankserver (192.168.0.235) — **Access:** `http://192.168.0.235:8096`
|
|
public vhost `jelly.franksdank.cloud` behind the [[Anubis]] guard chain →
|
|
[[Authentik]] outpost (post-auth route proxies `localhost:8096`).
|
|
|
|
## Facts (verified from the live unit, 2026-09-03)
|
|
|
|
| Item | Value |
|
|
|------|-------|
|
|
| Unit | `~/.config/containers/systemd/jellyfin.container` → `jellyfin.service` (user manager), `Restart=always`, `RestartSec=5` |
|
|
| Image | `ghcr.io/jellyfin/jellyfin:latest` |
|
|
| Network | `dankserver_appnet`; publishes 8096/tcp, 8920/tcp, 7359/udp, 1900/udp |
|
|
| Data roots | `/home/dank/Containers/jellyfin/config:/config`, `/home/dank/Containers/jellyfin/cache:/cache` |
|
|
| Media | `Volume=/var/lib/media-meta/view:/data` — the [[Media Metadata View]] overlay, **writable**; sidecars land on the NVMe, the [[Entropy Volume]] array underneath stays read-only and untouched |
|
|
| GPU | none — no `AddDevice`; CPU decode only (jellyfin-ffmpeg path) |
|
|
| Env quirks | `DOTNET_SYSTEM_IO_DISABLEFILELOCKING=true`, `LD_PRELOAD=libjemalloc.so.2`, `MALLOC_TRIM_THRESHOLD_=131072` kept verbatim from the compose-era container (zero-drift cutover) |
|
|
| Health | `curl -f http://localhost:8096/health` every 30 s |
|
|
|
|
## Data layout
|
|
|
|
```
|
|
/home/dank/Containers/jellyfin/config -> /config (JELLYFIN_DATA_DIR)
|
|
/home/dank/Containers/jellyfin/cache -> /cache (JELLYFIN_CACHE_DIR, XDG_CACHE_HOME)
|
|
/var/lib/media-meta/view -> /data (the SSD-backed overlay view)
|
|
```
|
|
|
|
Library folders point at `/data` paths, which are the view's paths: stable
|
|
across container recreation, and identical to the raw array layout below it.
|
|
|
|
`Local metadata` — save metadata to media folders — is enabled, and it works
|
|
because `/data` is the [[Media Metadata View]] rather than the read-only array:
|
|
`movie.nfo`, `tvshow.nfo`, `episode.nfo` and artwork are written once, on the
|
|
NVMe upper layer, and [[Plex]] reads the same files. Jellyfin is the fleet's
|
|
only metadata writer.
|
|
|
|
**Failure signature:** `System.IO.IOException: Read-only file system :
|
|
'/data/Movies/<film>/movie.nfo'` means the container is binding the raw array
|
|
instead of the view — the view is unmounted or the quadlet drifted. That used to
|
|
be the expected state (pre-view, every item update logged it); today it is a
|
|
fault. Run `scripts/media-view.sh status`.
|
|
|
|
## Ports & access
|
|
|
|
| Port | Proto | Purpose |
|
|
|------|-------|---------|
|
|
| 8096 | TCP | Web UI + API (`JELLYFIN_PublishedServerUrl=http://192.168.0.235:8096`) |
|
|
| 8920 | TCP | DLNA/UPnP root device control |
|
|
| 7359 | UDP | service discovery (client auto-discovery) |
|
|
| 1900 | UDP | SSDP discovery |
|
|
|
|
Client access paths and the public-exposure policy (auth wall required before
|
|
any public vhost; Plex-style claim tokens do not exist here — Jellyfin uses its
|
|
own users/API keys) follow the fleet [[Authentik]]/[[Anubis]] chain pattern.
|
|
|
|
## Authentik LDAP authentication (2026-09-25)
|
|
|
|
Jellyfin has no native external-auth, so it authenticates users through the
|
|
**LDAP-Auth plugin** (`jellyfin-plugin-ldapauth`) against an [[Authentik]] LDAP
|
|
provider. A user logs in with their **authentik username + password**; the
|
|
plugin binds as a service account, searches the directory, and rebinds as the
|
|
user to verify the password.
|
|
|
|
**authentik side** (on Dankclaw):
|
|
- LDAP provider `Jellyfin LDAP` (Base DN `dc=franksdank,dc=cloud`, bind/search
|
|
mode `direct`) + application `Jellyfin`.
|
|
- LDAP outpost `Jellyfin LDAP Outpost` → container `authentik-outpost-ldap`
|
|
(quadlet `dankclaw/authentik/quadlet/authentik-outpost-ldap.container`)
|
|
publishing `389`/`636` on the Dankclaw host for cross-host binds.
|
|
- Service account `svc-jellyfin-ldap` (DN
|
|
`cn=svc-jellyfin-ldap,ou=users,dc=franksdank,dc=cloud`).
|
|
- Access groups `jellyfin_users` (search filter
|
|
`(memberOf=cn=jellyfin_users,ou=groups,dc=franksdank,dc=cloud)`) and
|
|
`jellyfin_admins` (admin filter `(cn=jellyfin_admins)`). Add a user to
|
|
`jellyfin_users` to grant them Jellyfin access; `jellyfin_admins` grants
|
|
the Jellyfin admin role.
|
|
|
|
**Jellyfin side** (plugin config, set via admin → Plugins → LDAP-Auth):
|
|
- Server `192.168.0.143:389` (plain LDAP over the trusted LAN), Base DN
|
|
`dc=franksdank,dc=cloud`, bind user = the service-account DN, search
|
|
attributes `uid,cn,mail,displayName`, uid attr `uid`, username attr `cn`
|
|
`CreateUsersFromLdap=true`, all folders enabled.
|
|
|
|
**Gotcha — the bind credential is a PASSWORD, not a token.** The LDAP bind is
|
|
verified by authentik's default authentication flow (standard-password
|
|
backend). The service account must have a password set (admin → user →
|
|
Recovery → Set password); an API/app token is *not* accepted as the bind
|
|
password and the bind fails with `flow error password: Invalid`. The
|
|
service-account password is stored only in the Jellyfin plugin config (the
|
|
`LdapBindPassword` field) — it is never committed to this repo.
|
|
|
|
**Verification (2026-09-25):** `ldapsearch` bind + the `jellyfin_users`
|
|
search both succeed from Dankclaw; a real user (`alex`, in `jellyfin_users`)
|
|
logged into the web UI with their authentik password and the plugin created
|
|
the matching Jellyfin user.
|
|
**Gotcha — the login endpoint changed in Jellyfin 12.1.** The legacy
|
|
`POST /Users/Auth/Password` route (and its `LegacyEmbyRouteRewriteMiddleware`)
|
|
was removed; probing it now returns a silent 401 with no plugin log, which
|
|
reads like an auth failure but is just a dead route. The web UI and modern
|
|
clients use `POST /Users/AuthenticateByName` (with the `Authorization:
|
|
MediaBrowser Client=…, DeviceId=…, Device=…, Version=…` header), which is
|
|
where the LDAP plugin runs. Verified 2026-09-25: `alex` authenticates over
|
|
`AuthenticateByName` (HTTP 200; full plugin flow logged — service bind,
|
|
search, `ldapUid`, user bind, token).
|
|
|
|
## Configuration workflow
|
|
|
|
1. Create libraries pointing at `/data` folders (movies, shows, audio).
|
|
2. Set metadata/language preferences (`LANG=en_US.UTF-8` pinned in the unit).
|
|
3. Configure clients to use `http://192.168.0.235:8096` or the guarded vhost.
|
|
|
|
## See Also
|
|
|
|
- [[Plex]] — co-resident media server on the same volume
|
|
- [[Entropy Volume]] — the 91 T array mounted read-only at `/data`
|
|
- [[Caddy]] · [[Anubis]] · [[Authentik]] — edge chain for `jelly.franksdank.cloud`
|