Files
doc/nuc/jellyfin-server.md
Julien LutranandClaude Opus 5 9468f30f92 jellyfin: scan on download completion — inotify cannot work over NFS
Finished downloads stopped appearing in Jellyfin after the 2026-08-30
storage move, and everything looked healthy: the file was on nas, visible
through NFS inside the container, and /media/downloads is a configured
library. The cause is that Jellyfin watches libraries with inotify, which
only reports changes made through the local mount — transmission now
writes on nas while jellyfin-server reads over NFS on nuc, so no event
ever reaches it. EnableRealtimeMonitor is true and SupportsLibraryMonitor
reports true, which is why it looks fine. Previously both shared one local
dataset on nuc and it worked.

transmission now calls Jellyfin's /Library/Refresh via script-torrent-done.
The hook always exits 0 and never blocks (transmission runs it
synchronously; a hanging hook stalls the daemon), and both the success and
missing-key paths are tested. nuc is usually powered off, so a failed
request is expected and logged rather than treated as an error — the
scheduled scan catches up.

Also records that nuc's mount is read-only, so reorganising downloads into
movies/tv-shows must now happen on nas, and the ordered checklist for
"my download is not in Jellyfin" — the first three checks all passed when
this was hit, which is what made it confusing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:21:36 +02:00

8.2 KiB

jellyfin-server

Jellyfin media server in an unprivileged Incus container on nuc.

  • Image: images:ubuntu/24.04, Jellyfin from the official repo (repo.jellyfin.org)
  • IP: 192.168.0.5 (LAN bridge) — web UI/API on http://192.168.0.5:8096
  • iGPU render node (/dev/dri/renderD128) passed for QSV/VAAPI hardware transcoding
  • Media library: host /srv/media — since 2026-08-30 an NFSv4 mount from nas (192.168.0.4:/media, dataset tank/media), mounted at /media in the container with shift=false and readonly=true. See nas/nas-install.md §8.
  • Port 8096 additionally proxied to the host address (web proxy device)

Install script

Run as root on the Incus host: MEDIA_DIR=/srv/media ./install.sh

#!/usr/bin/env bash
# Adapted from:
# https://forgejo.benoit.jp.net/benoitjpnet/Laminar/src/branch/main/cfg/jobs/jellyfin.run
set -euxo pipefail

CNAME="${CNAME:-jellyfin-server}"
IMAGE="${IMAGE:-images:ubuntu/24.04}"
MEDIA_DIR="${MEDIA_DIR:-/srv/media}"   # host directory containing your media library

[ -d "$MEDIA_DIR" ] || { echo "MEDIA_DIR=$MEDIA_DIR does not exist on the host"; exit 1; }

incus launch "$IMAGE" "$CNAME"

# Wait until the container has working DNS/network
for i in $(seq 1 30); do
    incus exec "$CNAME" -- getent hosts repo.jellyfin.org >/dev/null 2>&1 && break
    sleep 2
done

incus config set "$CNAME" environment.DEBIAN_FRONTEND=noninteractive
incus exec "$CNAME" -- timedatectl set-timezone Europe/Paris
incus config set "$CNAME" environment.DEBCONF_NONINTERACTIVE_SEEN=true

# --- Jellyfin from the official repository -----------------------------------
incus exec "$CNAME" -- apt-get update
incus exec "$CNAME" -- apt-get upgrade -y
incus exec "$CNAME" -- apt-get install -y --no-install-recommends curl gnupg ca-certificates
incus exec "$CNAME" -- mkdir -p /etc/apt/keyrings
incus exec "$CNAME" -- bash -c 'curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg'
incus exec "$CNAME" -- bash -c 'cat > /etc/apt/sources.list.d/jellyfin.sources <<EOF
Types: deb
URIs: https://repo.jellyfin.org/ubuntu
Suites: noble
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF'
incus exec "$CNAME" -- apt-get update
incus exec "$CNAME" -- apt-get install -y jellyfin

# Optional: OpenCL runtime, only needed for HDR tone-mapping during transcodes
incus exec "$CNAME" -- apt-get install -y --no-install-recommends intel-opencl-icd || true

# --- iGPU render node for hardware transcoding (QSV/VAAPI) -------------------
RENDER_GID="$(incus exec "$CNAME" -- getent group render | cut -d: -f3)"
incus config device add "$CNAME" igpu gpu gid="$RENDER_GID"
incus exec "$CNAME" -- usermod -aG render,video jellyfin

# --- Media library (read-only is fine unless you let Jellyfin save NFO/artwork
#     next to the files) --------------------------------------------------
# NOTE: shift=true requires idmapped-mount support on MEDIA_DIR's filesystem —
# fine on local fs (ext4/btrfs/xfs), NOT supported on CIFS/NFS mounts.
incus config device add "$CNAME" media disk source="$MEDIA_DIR" path=/media shift=true

# --- Expose the web UI/API on the host's LAN address -------------------------
incus config device add "$CNAME" web proxy listen=tcp:0.0.0.0:8096 connect=tcp:127.0.0.1:8096

incus config set "$CNAME" boot.autostart=true
incus restart "$CNAME"

echo "Done. Open http://<host-ip>:8096 to run the setup wizard."

Media over NFS (2026-08-30)

The library moved to nas when the 4 TB left nuc's USB enclosure. The container keeps the same path, so everything below still applies — only the mount underneath /srv/media changed.

# nuc host: /etc/fstab
192.168.0.4:/media /srv/media nfs4 ro,_netdev,soft,timeo=100,retrans=3 0 0

shift=true cannot be used: idmapped mounts are not supported on NFS (nor CIFS). Per the troubleshooting note below, dropping the shift is enough for a read-only library — the export uses all_squash so files carry synthetic world-readable ownership:

incus stop jellyfin-server                 # shift cannot be hot-applied
incus config device set jellyfin-server media shift=false
incus config device set jellyfin-server media readonly=true
incus start jellyfin-server

⚠️ Two traps:

  • Boot ordering. Add remote-fs.target to nuc's /etc/systemd/system/incus.service.d/after-zfs.conf, or the container starts against an empty mountpoint and Jellyfin shows an empty library (and may prune the library metadata).
  • soft is deliberate. A hung nas should fail Jellyfin's reads, not wedge nuc's processes in uninterruptible sleep the way the suspended usb4t pool did (usb4t-dropouts.md).

transmission-bt is no longer on nuc — it moved to nas and writes to the dataset locally (nas/transmission-bt.md), so nuc's mount is read-only and there is exactly one writer.

⚠️ Real-time monitoring does not work over NFS (2026-08-31)

Libraries have EnableRealtimeMonitor=true and Jellyfin reports SupportsLibraryMonitor: true, but new files never appear on their own. Jellyfin watches with inotify, which only reports changes made through the local mount; transmission writes them on nas, so nuc's NFS client sees nothing. Jellyfin looks healthy and silently misses everything until a scan.

This is a regression from the 2026-08-30 storage move — before it, transmission and jellyfin-server shared one local dataset on nuc and inotify fired normally.

Fix in place: transmission calls Jellyfin's /Library/Refresh when a download completes — see nas/transmission-bt.md. Downloads appear within seconds; if nuc is powered off the request fails harmlessly and the scheduled scan catches up.

Manual scan (UI): Dashboard → Scheduled Tasks → Scan Media Library. By API:

curl -X POST -H "X-Emby-Token: <key>" http://192.168.0.5:8096/Library/Refresh   # expect 204

Diagnosing "my download is not in Jellyfin", in order — the first three were all fine when this was hit, which is what made it confusing:

ls /export/media/downloads/                                   # on nas: file there?
incus exec jellyfin-server -- ls /media/downloads/            # visible through NFS?
incus exec jellyfin-server -- find /var/lib/jellyfin/root -name '*.mblink' -exec cat {} +   # in a library path?
incus exec jellyfin-server -- cat /var/lib/jellyfin/data/ScheduledTasks/*.js | grep -o '"Name":"Scan Media Library".*'  # when did it last scan?

⚠️ nuc's mount is read-only. Reorganising finished downloads into /media/movies or /media/tv-shows can no longer be done from nuc — do it on nas under /export/media/.

First-run configuration

  1. Run the setup wizard; add libraries pointing at /media/....
  2. Dashboard → Playback → Transcoding:
    • Hardware acceleration: Intel QuickSync (QSV) (fallback: VA-API), device /dev/dri/renderD128.
    • Enable hardware decoding for the codecs you use; Alder Lake-N does H.264/HEVC/VP9/AV1 decode and H.264/HEVC encode.
    • Enable "Low-Power" encoders only if GuC/HuC is loaded (see below).

Troubleshooting / notes

  • Failed to setup device mount "media": idmapping abilities are required but aren't supported on system — the media source is on a filesystem without idmapped-mount support (CIFS/NFS). Either move the media to a local fs, or drop the shift (incus config device set jellyfin-server media shift=false; on CIFS files are world-readable synthetic ownership, enough for a read-only library). Hot-applying shift on a running container fails — stop it first.
  • QSV "low-power" encode fails — Alder Lake-N needs GuC/HuC firmware submission. On the host: echo 'options i915 enable_guc=3' > /etc/modprobe.d/i915.conf, update-initramfs -u, reboot; verify with dmesg | grep -i 'guc\|huc'. Or just untick the low-power options.
  • GPU check: incus exec jellyfin-server -- ls -l /dev/dri.
  • Pin a version with apt-get install jellyfin=<ver>+ubu2404 for reproducibility; keep major versions in sync with the client's JMP.
  • To bake a reusable image instead: run the script, then incus publish jellyfin-server --alias jellyfin-server-<ver>.