#!/usr/bin/env bash # # Jellyfin on TrueNAS with media on HDD and everything else on NVMe # https://www.zadran.net/guides/jellyfin-on-truenas-split-nvme-hdd # Generated from the guide, last updated 2026-09-13. # # NO WARRANTY. This script is provided as is. You are responsible for # understanding what it does before running it, and for the consequences. # Take a backup first. See https://www.zadran.net/disclaimer # # Destructive steps are left commented out on purpose -- uncomment them # only once you have understood what they do. set -euo pipefail # ---- Set these for your environment ---- # ZFS pool name for config, cache and transcode NVME_POOL='nvme' # ZFS pool name holding the media HDD_POOL='tank' # Where the NVMe pool is mounted NVME_POOL_PATH='/mnt/nvme' # Where the HDD pool is mounted HDD_POOL_PATH='/mnt/tank' # UID the container runs as; must be able to read the media PUID='1000' # GID the container runs as PGID='1000' # Affects scheduled task times and log timestamps TIMEZONE='Etc/UTC' # Pin a specific version rather than latest, so nothing changes under you JELLYFIN_VERSION='12.0' zfs create -p ${NVME_POOL}/apps/jellyfin/config zfs create -p ${NVME_POOL}/apps/jellyfin/cache zfs create -p ${NVME_POOL}/apps/jellyfin/transcode zfs create -p ${HDD_POOL}/media zfs create ${HDD_POOL}/media/movies zfs create ${HDD_POOL}/media/tv zfs create ${HDD_POOL}/media/kids zfs create ${HDD_POOL}/media/music-videos mkdir -p ${NVME_POOL_PATH}/apps/jellyfin cat > ${NVME_POOL_PATH}/apps/jellyfin/compose.yaml <<'EOF' services: jellyfin: image: jellyfin/jellyfin:${JELLYFIN_VERSION} container_name: jellyfin restart: unless-stopped network_mode: host user: "${PUID}:${PGID}" environment: - TZ=${TIMEZONE} - JELLYFIN_CACHE_DIR=/cache volumes: - ${NVME_POOL_PATH}/apps/jellyfin/config:/config - ${NVME_POOL_PATH}/apps/jellyfin/cache:/cache - ${NVME_POOL_PATH}/apps/jellyfin/transcode:/transcode - ${HDD_POOL_PATH}/media:/media:ro EOF cd ${NVME_POOL_PATH}/apps/jellyfin docker compose up -d docker compose logs -f docker exec jellyfin ls -la /media docker inspect -f '{{{{range .Mounts}}}}{{{{.Destination}}}} <- {{{{.Source}}}}{{{{"\n"}}}}{{{{end}}}}' jellyfin # !! DESTRUCTIVE -- review, then uncomment to run: # docker compose stop jellyfin # rm -rf ${NVME_POOL_PATH}/apps/jellyfin/cache/* # docker compose start jellyfin docker exec {{CONTAINER_NAME}} ls -l /dev/dri docker exec {{CONTAINER_NAME}} id while true; do line=$(docker top {{CONTAINER_NAME}} 2>/dev/null | grep -i ffmpeg) g=$(cat /sys/class/drm/card0/gt_act_freq_mhz 2>/dev/null \ || cat /sys/class/drm/card0/gt/gt0/rps_act_freq_mhz 2>/dev/null || echo n/a) if [ -n "$line" ]; then c=$(echo "$line" | grep -oE 'qsv|vaapi|libx264|libx265' | sort -u | tr '\n' ' ') echo "$(date +%T) ffmpeg running codecs=[$c] gpu=${g}MHz" else echo "$(date +%T) idle or copying gpu=${g}MHz" fi sleep 2 done ps -eo args | grep ffmpeg | grep -v grep