Files
serverAppConfigs/dankserver/jellyfin/quadlet/jellyfin.container
T
Frank 3e4b4f24f7 feat(media): share one metadata set between Jellyfin and Plex over an SSD overlay
Both media servers read a Kodi-format sidecar (movie.nfo / tvshow.nfo /
episode.nfo) only when it sits next to the media file, and the library lives on
the RAID6 array, deliberately mounted read-only for every container and slow to
seek for a thousand tiny XML reads. So neither server binds the array any more:
both bind /mnt/media, an OverlayFS merge of the array (lowerdir, never written)
with /var/lib/media-meta on the NVMe (upperdir, where every write lands).

Jellyfin binds the view rw at /data and is the fleet's only metadata writer;
Plex binds it ro at /mnt/entropy, the container path its own database already
recorded, so only the host side of that bind moved and its library paths keep
resolving.

scripts/media-view.sh plans, installs, verifies, reports and reaps the view. It
refuses to mount over an unmounted array, refuses an upper layer on rotational
storage, and refuses a non-root install. deploy-remote.sh raises the view before
it restarts either media server, both units carry
ConditionPathIsMountPoint=/mnt/media so a missing view skips the start instead of
letting a scan clear the library database, and the planner routes the installer
and dankserver/host/** to the jellyfin+plex pair.

tests/media_overlay.bats (11 seams) pins the resolved shape, both refusals, the
two binds and their guard, the deployer's ordering, the planner mapping, and the
reap behaviour against a fixture (ghosts listed, then moved to the SSD trash --
files that exist only above are sidecars, not ghosts, so only directories are
candidates).
2026-10-30 21:41:51 -07:00

64 lines
2.4 KiB
INI

# Quadlet unit: Jellyfin media server (rootless, boot-persistent).
#
# Install path on dankserver: ~/.config/containers/systemd/jellyfin.container
# Unit name: jellyfin.service (podman 5.x drops the container- prefix)
#
# Data roots migrated from /opt/jellyfin to /home/dank/Containers/jellyfin at
# cutover. The library is seen through /mnt/media, the SSD-backed overlay view
# (scripts/media-view.sh): the array stays read-only and untouched, while the
# sidecars this server writes land on the NVMe. Every
# creation-time environment variable of the compose-era container is kept as
# an Environment= line (zero drift); none of them are secrets, so this unit
# needs no EnvironmentFile.
[Unit]
Description=Jellyfin media server container
After=network.target
[Container]
ContainerName=jellyfin
Image=ghcr.io/jellyfin/jellyfin:latest@sha256:fea863d0cd9ec445985d9c75fc6ed64e38a1d2766cf0c250a33d553f4fc1b771
Network=dankserver_appnet
Environment=DEBIAN_FRONTEND=noninteractive
Environment=DOTNET_SYSTEM_IO_DISABLEFILELOCKING=true
Environment=JELLYFIN_CACHE_DIR=/cache
Environment=JELLYFIN_CONFIG_DIR=/config/config
Environment=JELLYFIN_DATA_DIR=/config
Environment=JELLYFIN_FFMPEG=/usr/lib/jellyfin-ffmpeg/ffmpeg
Environment=JELLYFIN_LOG_DIR=/config/log
Environment=JELLYFIN_PublishedServerUrl=http://192.168.0.235:8096
Environment=JELLYFIN_WEB_DIR=/jellyfin/jellyfin-web
Environment=LANG=en_US.UTF-8
Environment=LANGUAGE=en_US:en
Environment=LC_ALL=en_US.UTF-8
Environment=LD_PRELOAD=/usr/lib/jellyfin/libjemalloc.so.2
Environment=MALLOC_TRIM_THRESHOLD_=131072
Environment=NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
Environment=NVIDIA_VISIBLE_DEVICES=all
Environment=XDG_CACHE_HOME=/cache
PublishPort=8096:8096
PublishPort=8920:8920
PublishPort=7359:7359/udp
PublishPort=1900:1900/udp
Volume=/home/dank/Containers/jellyfin/config:/config
Volume=/home/dank/Containers/jellyfin/cache:/cache
# The view, writable: Jellyfin is the metadata writer (movie.nfo / tvshow.nfo /
# episode.nfo + artwork) and those bytes belong on the SSD. The condition is the
# fail-closed guard — starting against a plain /mnt/media directory would let a
# scan conclude the library is empty.
Volume=/mnt/media:/data
UnitConfigItem=ConditionPathIsMountPoint=/mnt/media
AddDevice=/dev/dri
GroupAdd=keep-groups
HealthCmd=curl -f http://localhost:8096/health
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
[Service]
Restart=always
RestartSec=5
[Install]
WantedBy=default.target