Files
doc/nuc/jellyfin-server.md
T
Julien LutranandClaude Opus 5 203728674f nuc: media over NFS from nas, on-demand role, i915 and input traps
nuc keeps only what needs its iGPU. blocky, privoxy and transmission-bt
moved to nas so the box can be powered off when not watching Jellyfin or
using the Spotify kiosk.

- /srv/media is now an NFSv4 mount from nas; jellyfin-server reads it
  with shift=false (idmapped mounts are unsupported on NFS, as the
  container doc already noted for CIFS) and readonly=true
- replication to nas is a systemd timer with Persistent=true, not cron —
  an on-demand host misses its 03:30 window and cron cannot catch up

Two failures documented in full, both diagnosed from the wrong layer
first:

- booting with the TV connected and powered on kills the i915 probe
  (drm_WARN_ON in intel_modeset_setup_hw_state), so /dev/dri never
  appears, snd_hda_intel deferred-probes forever holding the PCI device
  lock, and incusd blocks in sriov_numvfs_show — no container starts at
  all, including LAN DNS. Identical on 6.12.107 and 6.12.105.
- the kiosk input gid mismatch is real but was NOT the cause of the
  2026-08-30 outage (flat K400 batteries were); seatd opens input devices
  as root, so kiosk group membership is not on that path. Records the
  one-line raw capture that settles hardware-vs-software immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 23:53:30 +02:00

6.4 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.

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>.