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.
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. |
|
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 DNdc=franksdank,dc=cloud, bind/search modedirect) + applicationJellyfin. - LDAP outpost
Jellyfin LDAP Outpost→ containerauthentik-outpost-ldap(quadletdankclaw/authentik/quadlet/authentik-outpost-ldap.container) publishing389/636on the Dankclaw host for cross-host binds. - Service account
svc-jellyfin-ldap(DNcn=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)) andjellyfin_admins(admin filter(cn=jellyfin_admins)). Add a user tojellyfin_usersto grant them Jellyfin access;jellyfin_adminsgrants 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 DNdc=franksdank,dc=cloud, bind user = the service-account DN, search attributesuid,cn,mail,displayName, uid attruid, username attrcnCreateUsersFromLdap=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 orpsoutput has leaked: rotate it with the provider, rewriteprovider-keys, re-runinstall. - The TVDB artifact is pinned three ways: sha256
6302fba3857bdde8d186dd099fac09ca43247041f531a528a2c788add70a5319, the official plugin guida677c0da-fac5-4cde-941a-7134223f14c8, and version24.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: movethetvdb_24.0.0.0/into the trash and revert the pin; a secondthetvdb_*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 optionalTVDB_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_keysinscripts/deploy-remote.sh) and warns instead of failing whenprovider-keysis absent, so a host without it still deploys. - Keys and plugins are read at start-up: after a manual
installthe 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
- Create libraries pointing at
/datafolders (movies, shows, audio). - Set metadata/language preferences (
LANG=en_US.UTF-8pinned in the unit). - Configure clients to use
http://192.168.0.235:8096or 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