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>
148 lines
6.4 KiB
Markdown
148 lines
6.4 KiB
Markdown
# 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](../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`
|
|
|
|
```bash
|
|
#!/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.
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```sh
|
|
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](usb4t-dropouts.md)).
|
|
|
|
`transmission-bt` is no longer on nuc — it moved to nas and writes to
|
|
the dataset locally ([nas/transmission-bt.md](../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>`.
|