Files
serverAppConfigs/serverAppConfigs-docs/Dankserver/Jellyfin.md
T
Frank 949d3cb6f4 docs(jellyfin): one page owns the provider keys and where they live
The page carries the deploy-time shape: which four providers are core plugins
in Jellyfin 12, which one is not, the exact XML each key lives in, the host
file the keys come from and its 600 rule, the three-way pin on the TVDB
artifact with its rollback, and what the deployer does when the keys file is
absent. OMDb is recorded as deliberately unconfigured -- keyed by email,
1000 req/day, and a ToS that forbids storing what it returns. index.md's row
carries the same locator.
2026-10-30 22:47:49 -07:00

10 KiB

type, title, description, tags, timestamp
type title description tags timestamp
reference Jellyfin Jellyfin on Dankserver: quadlet unit, overlay metadata view, provider keys, ports, access, transcode status.
jellyfin
dankserver
media-server
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).

Metadata providers and their keys (2026-10-30)

TMDb, OMDb, StudioImages and MusicBrainz are core plugins in Jellyfin 12 — no DLL, just a configuration file the running container owns under the data root. TVDB is not core (its metadata provider was removed in 10.9), so it is an official-catalog plugin.

Provider Shape on disk Key lives in
TMDb config/plugins/configurations/Jellyfin.Plugin.Tmdb.xml → <TmdbApiKey> /home/dank/Containers/jellyfin/provider-keys → TMDB_API_KEY= (mode 600)
TVDB config/plugins/thetvdb_24.0.0.0/ + …Jellyfin.Plugin.Tvdb.xml → <ApiKey> the same file → TVDB_API_KEY=
OMDb …Jellyfin.Plugin.Omdb.xml not configured — its key is issued by email, 1 000 req/day, and its ToS forbids storing the returned data; ratings stay Jellyfin's own lookup

None of it is tracked in git. Those XML files belong to the container and hold credentials, and tests/jellyfin_providers.bats fails the suite if one is ever committed. The keys themselves sit in the per-service host env file that is the fleet's convention for container credentials (grafana, caddy, pihole, copyparty, romm, seerr): provider-keys, mode 600, beside the data root. scripts/jellyfin-providers.sh is the only thing that writes the XML:

scripts/jellyfin-providers.sh plan      # what would change; prints no key
scripts/jellyfin-providers.sh install   # plugin + keys, previous copy to trash
scripts/jellyfin-providers.sh verify    # plugin + key LENGTHS, never key bytes
  • A key never enters git, argv, or the tool's output. The installer reads that 600-perm file itself (a value in argv is world-readable through /proc/<pid>/cmdline) and refuses it when it is group- or world-readable — an exposed key is an error, not a warning. A key that shows up in a transcript or ps output has leaked: rotate it with the provider, rewrite provider-keys, re-run install.
  • The TVDB artifact is pinned three ways: sha256 6302fba3857bdde8d186dd099fac09ca43247041f531a528a2c788add70a5319, the official plugin guid a677c0da-fac5-4cde-941a-7134223f14c8, and version 24.0.0.0 (targetAbi 12.0.0.0, which fits the deployed 12.4.0). A sha match alone only proves the bytes are the bytes fetched — the guid check is what says it is the official plugin. Rollback: move thetvdb_24.0.0.0/ into the trash and revert the pin; a second thetvdb_* directory is a refusal, never a quiet second copy.
  • The homelab TVDB key is a licensed key (its JWT claims say community_supported: false), so it needs no PIN — the optional TVDB_PIN= line exists only for a subscriber-shaped key.
  • TVDB is installed and keyed but is not a metadata agent: TMDb still identifies everything. Enabling TVDB as a provider would re-fetch show metadata across the library, so it is a deliberate, separate change.
  • The deployer places the keys before it restarts jellyfin (ensure_provider_keys in scripts/deploy-remote.sh) and warns instead of failing when provider-keys is absent, so a host without it still deploys.
  • Keys and plugins are read at start-up: after a manual install the change is inert until ./scripts/deploy.sh dankserver --service jellyfin.
  • TMDb's terms are the reason this stays narrow: attribution required, no caching beyond 6 months, no derivative works. Fine for a personal homelab, and the reason no bulk enrichment pass runs against it.

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