Skip to content

Jellyfin — Setup

Media server with NVIDIA GPU-accelerated transcoding. Runs on a dedicated Debian LXC (114) with GTX 750 Ti passthrough. Accessible at https://jellyfin.eva-00.network.

Infrastructure

Host LXC ID IP URL Resources
Debian 12 LXC 114 192.168.1.114 https://jellyfin.eva-00.network 6 cores, 8 GB RAM, 50 GB disk

GPU Capabilities

Feature GTX 750 Ti (GM107, Maxwell 1st gen)
H.264 encode Yes
H.264 decode Yes
HEVC (H.265) No — CPU fallback
VP9 No — CPU fallback
AV1 No — CPU fallback
VRAM 2 GB
Concurrent 1080p H.264 transcodes ~2-3
NVIDIA driver 535.154.05
Docker runtime nvidia

GPU is passed through at the LXC level (not VM). The host-side GPU setup is managed by a separate playbook and workflow.

GPU Passthrough Guide

This section documents every step needed to pass an NVIDIA GPU from the Proxmox host into an unprivileged LXC. All steps are codified in IaC (playbooks below), but this guide explains what each step does and why.

Prerequisites

  • NVIDIA GPU physically installed in the Proxmox host
  • LXC must be privileged or have the correct cgroup device rules (unprivileged works with the config below)
  • LXC must be stopped before modifying its conf file

Step 1: Install NVIDIA driver on the Proxmox host

The host needs the kernel driver so the GPU device nodes (/dev/nvidia*) are created.

# On chizuru (Proxmox host)

# Add non-free repos (needed for NVIDIA packages)
sed -i 's/bookworm main$/bookworm main contrib non-free non-free-firmware/' /etc/apt/sources.list

# Install prerequisites + driver
apt update
apt install -y pve-headers build-essential dkms nvidia-driver nvidia-smi

# Load the UVM module (needed for CUDA/compute)
modprobe nvidia-uvm
echo "nvidia-uvm" > /etc/modules-load.d/nvidia.conf

# Verify
nvidia-smi

After install, four device nodes must exist:

Device Major Purpose
/dev/nvidia0 195 GPU device
/dev/nvidiactl 195 GPU control
/dev/nvidia-uvm 236 Unified Virtual Memory
/dev/nvidia-uvm-tools 236 UVM tools

Step 2: Configure LXC GPU passthrough

Edit the LXC conf on the Proxmox host to allow access to the GPU devices and bind-mount them into the container.

# On chizuru — edit /etc/pve/lxc/114.conf
# Add these lines (the playbook uses blockinfile):

# cgroup2 device allow rules (major device numbers)
lxc.cgroup2.devices.allow: c 195:* rwm
lxc.cgroup2.devices.allow: c 236:* rwm

# Bind mount GPU devices into the LXC
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file

Key details:

  • c 195:* rwm — allows character device major 195 (nvidia) with read/write/mknod
  • c 236:* rwm — allows character device major 236 (nvidia-uvm)
  • bind,optional,create=file — bind mounts the host device; optional means LXC starts even if device is absent
  • These go in the LXC conf file, not inside the container

Step 3: Install matching NVIDIA driver inside the LXC

The driver version inside the LXC must exactly match the host driver version. Since the LXC shares the host kernel, install with --no-kernel-modules.

# Inside LXC 114

# Check host driver version first
# (on host: nvidia-smi --query-gpu=driver_version --format=csv,noheader)
# Example: 535.154.05

# Download the exact matching version
wget https://us.download.nvidia.com/XFree86/Linux-x86_64/535.154.05/NVIDIA-Linux-x86_64-535.154.05.run
chmod +x NVIDIA-Linux-x86_64-535.154.05.run

# Install userspace libraries only (no kernel modules!)
./NVIDIA-Linux-x86_64-535.154.05.run --no-kernel-modules --silent --no-questions

# Verify GPU is visible
nvidia-smi

Why --no-kernel-modules: The LXC uses the host's kernel and kernel modules. Installing kernel modules inside the container would fail (no kernel headers) and is unnecessary.

Step 4: Install nvidia-container-toolkit for Docker

This allows Docker containers (like Jellyfin) to access the GPU via the nvidia runtime.

# Inside LXC 114

# Add NVIDIA container toolkit repo
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
  > /etc/apt/sources.list.d/nvidia-container-toolkit.list

apt update && apt install -y nvidia-container-toolkit

# Register the nvidia runtime with Docker
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

Step 5: Configure Docker Compose for GPU

The Jellyfin container uses runtime: nvidia and resource reservations:

services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    runtime: nvidia
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    environment:
      - NVIDIA_VISIBLE_DEVICES=all

Step 6: Media bind mount (Proxmox level)

Jellyfin reads media from mediabot's data directory via a Proxmox mount point:

# On chizuru — added to /etc/pve/lxc/114.conf:
# mp0: <mediabot-rootfs-path>/data,mp=/data/media,ro=1
#
# The playbook dynamically resolves the rootfs path:
#   grep -oP 'rootfs:\s+\K[^,]+' /etc/pve/lxc/113.conf → e.g. local-lvm:vm-113-disk-0
#   pvesm path local-lvm:vm-113-disk-0 → e.g. /dev/pve/vm-113-disk-0

After adding the mount point, restart LXC 114 for it to take effect.

Verification checklist

# On Proxmox host:
nvidia-smi                          # GPU visible, driver loaded
ls -la /dev/nvidia*                 # All 4 device nodes exist

# Inside LXC 114:
nvidia-smi                          # GPU visible (same output as host)
docker run --rm --runtime=nvidia nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
                                    # GPU visible inside Docker container

# Inside Jellyfin:
# Admin → Playback → Transcoding → NVIDIA NVENC should be available

IaC automation

Both playbooks automate all of the above:

Playbook What it does When to run
jellyfin-host-gpu.yml Steps 1-2 + 6 (host driver, LXC conf, bind mount) First-time setup or driver update
jellyfin.yml Steps 3-5 (LXC driver, nvidia-container-toolkit, Docker, Jellyfin deploy) Every deploy

Trigger via Forgejo Actions → workflow_dispatch for the host GPU playbook, or automatic on push for the Jellyfin deploy.

Observability

Logs

Jellyfin logs are collected via Grafana Alloy Docker discovery and shipped to Loki.

Query Purpose
{container="jellyfin"} All container output
{container="jellyfin"} \|= "error" Errors only
{container="jellyfin"} \|= "transcode" Transcoding activity
{container="jellyfin"} \|= "hardware" Hardware acceleration messages

Access: Grafana → Explore → Loki → Enter query

Health

Endpoint Expected
http://192.168.1.114:8096/health Healthy

IaC

Artifact Path
Playbook (deploy) ansible/playbooks/jellyfin.yml
Playbook (GPU host setup) ansible/playbooks/jellyfin-host-gpu.yml
Workflow (deploy) .forgejo/workflows/jellyfin.yml
Workflow (GPU host setup) .forgejo/workflows/jellyfin-host-gpu.yml (workflow_dispatch only)
Compose services/jellyfin/docker-compose.yml
CSS Themes services/jellyfin/themes/liquid-glass.css, services/jellyfin/themes/ios18-dark.css, services/jellyfin/themes/mediamanager.css

Secrets

Vault Path Key Purpose
secret/jellyfin api_key Jellyfin API key for automated plugin install and branding config
secret/jellyfin oidc_client_id PocketID OIDC client ID (created by pocketid-setup workflow)
secret/jellyfin oidc_client_secret PocketID OIDC client secret

Generate an API key after completing the setup wizard: Dashboard → API Keys → Create. Store it in Vault: vault kv patch secret/jellyfin api_key=<key>

CSS Themes

Two custom CSS themes are available in services/jellyfin/themes/:

Theme File Style
Liquid Glass liquid-glass.css iOS 26 Liquid Glass light mode — translucent white panels, backdrop blur, blue accents
iOS 18 Dark ios18-dark.css iOS 18 dark mode — true black background, no borders, vibrant system colors
MediaManager mediamanager.css Monochrome grayscale with MM brand accents (blue/orange/pink)

The Liquid Glass theme is applied automatically by the playbook via the branding API. To switch themes, update the playbook's lookup('file', ...) path or apply manually via Dashboard → Branding → Custom CSS.

Libraries

Library Type Source Metadata Notes
Anime tvshows /dlbox/normal/anime + /unohana/shoko/anime + /filedump/anime Shoko (Shokofin VFS) All anime TV series across temp + curated archive + filedump
Anime Movies movies /dlbox/normal/anime + /unohana/shoko/anime + /filedump/anime Shoko (Shokofin VFS) Same paths as Anime; Shokofin filters movies into here
Seasonal tvshows /dlbox/normal/seasonal Shoko (Shokofin VFS) Current-season anime auto-downloaded by Seanime
Movies movies /dlbox/normal/movies/general + /filedump/movies TMDb Live-action movies (mediabot + manual archive)
TV Shows tvshows /dlbox/normal/tv/live-action + /filedump/tv TVDB + TMDb Live-action shows (mediabot + manual archive)

The Anime / Anime Movies / Seasonal libraries use Shokofin in VFS mode. Shoko Server (LXC 116) indexes files from all four anime locations (registered as Shoko import folders 1, 3, 4, 5) and provides metadata via AniDB. The Anime library is populated by downloading torrents with the anime category in qBittorrent-normal (LXC 110); the Seasonal library is populated by Seanime (LXC 134) auto-downloading current-season anime via the seasonal qbit category.

Anime + Anime Movies share the same three paths. Jellyfin's content type filter, combined with Shokofin's FilterMovieLibraries: True, routes shows into Anime and movies into Anime Movies automatically. Standalone anime movies stay out of the Shows library this way.

Franchise grouping via Jellyfin Collections. Shokofin's CollectionGrouping: Shared auto-builds a Jellyfin Collection for each Shoko Group (franchise), spanning both Anime and Anime Movies. So an "Attack on Titan" Collection holds the TV series + the movies in one browsable view.

The Movies and TV Shows libraries are populated by mediabot (LXC 113), which writes to /dlbox/normal/movies/general and /dlbox/normal/tv/live-action. Each library also includes a folder under /filedump for content the user has manually archived to the 12TB HDD.

Anime subfolders managed by mediabot (/dlbox/normal/movies/anime, /dlbox/normal/tv/anime) are intentionally not added to the Movies / TV Shows libraries — they remain Shoko's responsibility.

Realtime anime pipeline (Seanime → Shoko → Shokofin → Jellyfin)

New episodes appear in Jellyfin within seconds of qBittorrent finishing the download. The chain has four moving parts, each of which must be configured correctly — if any one is wrong, episodes get stuck waiting for the next scheduled scan.

AniList watchlist
  → Seanime auto-downloader (poll every 20 min, sends to qbit-normal)
  → qbit-normal downloads to /dlbox/normal/seasonal
  → Shoko WatchForNewFiles (inotify on the import folder) indexes the file
  → Shoko emits SignalR file/refresh event
  → Shokofin (connected to Shoko's SignalR) regenerates the VFS symlink
    AND triggers a Jellyfin metadata refresh on the affected library
  → Jellyfin RealtimeMonitor on /config/Shokofin/VFS/<uuid>/ picks up the
    new symlink and registers the episode

Required Shokofin plugin settings (set by ansible/playbooks/jellyfin.yml):

Setting Value Effect
SignalR_AutoConnectEnabled true Shokofin keeps a SignalR connection to Shoko Server. Without this, no realtime events flow.
SignalR_RefreshEnabled true Plugin-level switch for refresh events. Off by default.
SignalR_FileEvents true Receive file-level events (new file, deleted, moved).
Per-library IsFileEventsEnabled true Library responds to file events → VFS symlink regen. Defaults to false on auto-created libraries.
Per-library IsRefreshEventsEnabled true Library responds to refresh events → Jellyfin scan trigger. Defaults to false. This is the easiest one to miss — without it the VFS gets the symlink but Jellyfin doesn't scan it until the next scheduled run.

Required Jellyfin library settings (also set by the playbook):

Setting Value Effect
EnableRealtimeMonitor true Jellyfin's inotify watcher on the library path. Fires on filesystem changes.
AutomaticRefreshIntervalDays 30 Background safety-net scan; should rarely actually catch new files because the realtime pipeline gets there first.

Verify the connection is live with:

pct exec 114 -- docker logs --since 5m jellyfin | grep SignalRConnectionManager

You should see Connected to Shoko Server. after the plugin starts (or after a config flip). If you see repeated Connecting to… lines without a Connected, Shoko at 192.168.1.116:8111 is unreachable or the Shokofin ApiKey is wrong.

MyAnimeList Plugin

The playbook automatically adds the MAL plugin repository and installs the plugin. MAL is available as a fallback metadata provider but is not currently used — both libraries use Shoko.

Repository URL: https://raw.githubusercontent.com/ryandash/jellyfin-plugin-myanimelist/refs/heads/main/manifest.json

SSO with PocketID

Jellyfin uses the SSO-Auth plugin for single sign-on via PocketID. The full SSO setup is automated via API — no manual UI configuration needed.

How it works

  1. pocketid-setup workflow: Creates the jellyfin OIDC client in PocketID with callback URL https://jellyfin.eva-00.network/sso/OID/redirect/PocketID, stores credentials in Vault
  2. jellyfin workflow: Installs the SSO-Auth plugin, configures it via API, adds the login button, and disables password login

SSO plugin configuration (via API)

The SSO-Auth plugin config is set via the Jellyfin plugin configuration API:

POST /Plugins/505ce9d1-d916-42fa-86ca-673ef241d7df/Configuration

The plugin GUID is 505ce9d1-d916-42fa-86ca-673ef241d7df. Key configuration:

Setting Value
OidEndpoint https://auth.eva-00.network
OidClientId From Vault: secret/jellyfinoidc_client_id
OidSecret From Vault: secret/jellyfinoidc_client_secret
Enabled true
EnableAuthorization true
EnableAllFolders true
RoleClaim groups
AdminRoles ["admin"] (maps PocketID admin group to Jellyfin admin)
OidScopes ["openid", "profile", "email", "groups"]
SchemeOverride https
DefaultUsernameClaim preferred_username

Login button (via Branding API)

The SSO-Auth plugin does not automatically add a login button. It must be injected via the Branding API's LoginDisclaimer field:

POST /System/Configuration/branding
{
  "LoginDisclaimer": "<form action=\"https://jellyfin.eva-00.network/sso/OID/start/PocketID\"><button class=\"raised block emby-button button-submit\" style=\"margin-top:1em;\">Sign in with PocketID</button></form>"
}

Disabling password login

Password login is disabled in two ways:

  1. Per-user: Set each user's AuthenticationProviderId to Jellyfin.Plugin.SSO.SSOAuthenticationProviderPlugin via POST /Users/{id}/Policy
  2. UI: Hide the username/password form via CSS in the Branding CustomCss field:
#loginPage .manualLoginForm .inputContainer,
#loginPage .manualLoginForm > .button-submit,
#loginPage a[href="#/forgotpassword.html"] {
  display: none !important;
}

Admin role mapping

The SSO plugin's AdminRoles setting maps PocketID groups to Jellyfin admin. Users in the PocketID admin group get Jellyfin admin on SSO login. Without this, the plugin resets IsAdministrator to false on each login.

Important limitations

  • Browser only: SSO login works in the Jellyfin web UI. Mobile apps (iOS/Android) and desktop clients do not support SSO — use QuickConnect instead (see below)
  • First login creates a new Jellyfin user: The SSO plugin auto-creates a Jellyfin user on first OIDC login using the preferred_username claim from PocketID
  • Existing users: If you already created a local user (e.g. holo), the SSO plugin will create a separate user. You can merge them by setting the same username in PocketID

QuickConnect (Mobile & Desktop Clients)

QuickConnect allows Jellyfin mobile apps and desktop clients to authenticate without entering a password — useful since these clients don't support SSO.

Enable QuickConnect (one-time)

  1. Go to Dashboard → Networking (or Dashboard → General depending on Jellyfin version)
  2. Find QuickConnect and ensure it is Enabled
  3. Save

How to use QuickConnect

On the client (phone/TV/desktop app):

  1. Open the Jellyfin app
  2. Enter the server URL: https://jellyfin.eva-00.network
  3. On the login screen, tap Quick Connect
  4. The app will display a 6-digit code

On the web UI (browser):

  1. Log in to Jellyfin via the web UI (using PocketID SSO or local password)
  2. Click your user icon (top right) → Quick Connect
  3. Enter the 6-digit code from the app
  4. Click Authorize

The app will automatically log in. The authorization is permanent for that device — you don't need to repeat it unless you sign out.

Supported clients

Client QuickConnect SSO Notes
Web browser N/A Yes Use PocketID SSO button on login page
Jellyfin iOS app Yes No Use QuickConnect to authenticate
Jellyfin Android app Yes No Use QuickConnect to authenticate
Jellyfin Android TV Yes No Use QuickConnect to authenticate
Jellyfin Desktop (Electron) Yes No Use QuickConnect to authenticate
Jellyfin MPV Shim No No Use local username/password
Finamp (music) Yes No Use QuickConnect to authenticate
Swiftfin (iOS alt) Yes No Use QuickConnect to authenticate

Revoking access

To revoke a device's QuickConnect authorization:

  1. Go to Dashboard → Devices
  2. Find the device
  3. Click Delete to revoke its session

Transcoding Configuration

Configure hardware transcoding in the Jellyfin admin dashboard:

  1. Go to Administration → Playback → Transcoding
  2. Set hardware acceleration to NVIDIA NVENC
  3. Enable H.264 hardware encoding and decoding
  4. Disable HEVC, VP9, and AV1 hardware options (not supported by the 750 Ti — these fall back to CPU automatically)
  5. Save and restart Jellyfin if prompted

Media Storage

Media is mounted read-only inside the LXC via Proxmox bind mounts. The actual paths live on chizuru's disks; the LXC sees them at the mount points below.

Mount (inside LXC) Host source Access Used by
/dlbox /mnt/seedbox (chizuru) Read-only Movies, TV Shows libraries (under /dlbox/normal/...); Anime via Shokofin VFS
/filedump /mnt/filedump (chizuru) Read-only Movies, TV Shows libraries (archived content)
/unohana /mnt/pve/urahara (chizuru) Read-only Archives (anime) via Shokofin VFS
/urahara /mnt/pve/urahara (chizuru) Read-only Reserved