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>
188 lines
8.2 KiB
Markdown
188 lines
8.2 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.
|
|
|
|
## ⚠️ 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](../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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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>`.
|