Skip to main content

Jellyfin

Jellyfin is a fully open-source, fork-of-Emby media server: video, music, and photo streaming to any device, with no account, no telemetry, and no licence wall. In this homelab it sits on top of an NFS share from TrueNAS (read-only media library) and a Longhorn-encrypted PVC (storage component) for its own config and metadata, exposed via Envoy Gateway on jellyfin.web.kueber.eu.

Why Jellyfin​

  • No account wall. Plex requires a Plex.tv account and brokers connections through their servers; Jellyfin is fully local and works without any external service.
  • Truly free. Plex Pass and Emby Premiere paywall features (hardware transcoding, mobile sync, live TV) that Jellyfin ships in the base build.
  • Client ecosystem is wide enough. Native apps for Android TV, iOS, Android, web, plus first-class support from Infuse, Findroid, Streamyfin, and (via jellycon) Kodi.
  • Plays well with the storage layout. TrueNAS exports the media share over NFS, Jellyfin mounts it read-only, and writes only its config + metadata into the cluster — keeps the per-app PVC small and easy to back up.

Alternatives considered​

OptionHostedSelf-hostedWhy not
Plexpartial✓Account required; Plex Pass paywalls hardware transcoding
Emby✓Closed-source after the Jellyfin fork; key features need Premiere
Netflix / streaming services✓Library churn; nothing offline; no control over codecs or quality

Installation​

The Jellyfin install is straightforward — one Deployment, two volumes, one HTTPRoute — but every bit of it leans on a primitive defined elsewhere:

  • Single Deployment in the jellyfin namespace, image pinned by digest. Renovate keeps the tag fresh.
  • Storage split across two volumes. A 150Gi ReadWriteOnce PVC on the longhorn-encrypted storage class (storage resource) for Jellyfin's own config and metadata, annotated k8up.io/backup: "true" so the backups component picks it up. The media library itself is an NFS PV pointing at TrueNAS, mounted read-only.
  • Routing via HTTPRoute to the public Envoy Gateway listener — TLS terminated at the edge by cert-manager on the gateway level, not in the pod.
  • No in-cluster database. Jellyfin keeps its state in an embedded SQLite next to the config PVC, so there's no CNPG dependency.
  • Image: linuxserver.io build. The container expects PUID/PGID to align ownership of the NFS mount and the config PVC; that means it runs as runAsUser: 0 with a chown at startup rather than as a non-root user. It's the pragmatic trade for the linuxserver build's PUID/PGID handling.

Administration​

  • Logging in for the first time. The first browser hit on a fresh deploy launches the setup wizard — pick a username, point Jellyfin at the NFS-mounted media paths, and let it scan. The scan is the long step on first run.
  • Adding a library. Admin Dashboard → Libraries → Add Media Library. Paths are the in-container mount points (/data/movies, /data/shows), not the host paths.
  • Hardware transcoding. Talos exposes the integrated Intel GPU via the intel-gpu-plugin DaemonSet; Jellyfin's Playback → Hardware acceleration picks it up as VA-API. Soft-fallback to software transcoding if the GPU isn't scheduled on the node.
  • SSO is not wired up. Jellyfin's own user store handles auth. The SSO-Auth plugin is an option for delegating to Keycloak, but the value is marginal for the handful of accounts on this server.
  • Upgrades. Digest-pinned image; Renovate auto-merges patch + digest bumps per the Renovate policy. Minor/major bumps need a human read — releases occasionally include schema migrations on the first start after upgrade.
  • Backups. k8up.io/backup: "true" on the config PVC is the only knob; the backups component schedules the restic snapshots to Hetzner and Backblaze. The media library on TrueNAS is not backed up via k8up — see Topics → Backups end-to-end for the full storage-tier story.

Usage​

  • Browser, mobile apps, and TV apps all just point at https://jellyfin.web.kueber.eu.
  • Direct-play whenever possible. Most modern clients (Infuse, Streamyfin, the Android TV app) handle H.264 / H.265 / AV1 natively, so the server hands the file off as-is. Only the lowest-common-denominator clients trigger transcoding.
  • Offline downloads. The official mobile apps support sync to local storage; useful before flights, less so day-to-day.
  • Metadata. Jellyfin pulls from TheMovieDB / TheTVDB at scan time and caches in the config PVC. Re-scanning a library re-fetches missing metadata but does not overwrite manual edits.

Cluster Deployment​

Jellyfin — Talos cluster

Deviations from defaults​

  • Transcodes on their own 50Gi PVC, jellyfin-transcodes, mounted at /config/data/transcodes. Leftover segments filled the 21Gi config PVC on 2026-10-15 and broke all playback. The PVC uses longhorn-object-single-encrypted: one replica next to the pod, encrypted with the namespace's object-longhorn key, and not backed up. Jellyfin empties it on every start. A full volume fails only the stream that is writing, so keep Throttle transcodes and Delete segments enabled in the dashboard. With both on, the worst 4K remux (~75 Mbit/s) holds ~8.4 GB. If the replica's node is lost for good, delete the PVC (and its retained PV) so a fresh one is created.
Kubernetes Metadata
  • Image: linuxserver/jellyfin:12.2ubu2604-ls54@sha256:799509f8ef3be7037478699cc6fd14d19dd75faac5e474ed219e98ae78a5b3e4
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app.kubernetes.io/name: jellyfin
name: jellyfin
namespace: jellyfin
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: jellyfin
strategy:
rollingUpdate: null
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: jellyfin
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: gpu.kueber.eu/av1-encode
operator: In
values:
- 'true'
weight: 100
automountServiceAccountToken: false
containers:
- env:
- name: PUID
value: '0'
- name: GUID
value: '0'
image: linuxserver/jellyfin:12.2ubu2604-ls54@sha256:799509f8ef3be7037478699cc6fd14d19dd75faac5e474ed219e98ae78a5b3e4
name: jellyfin
ports:
- containerPort: 8096
name: web
protocol: TCP
readinessProbe:
failureThreshold: 1
httpGet:
path: /
port: 8096
initialDelaySeconds: 2
periodSeconds: 3
successThreshold: 1
timeoutSeconds: 2
resources:
limits:
gpu.intel.com/i915: 1
memory: 4162Mi
requests:
cpu: 135m
gpu.intel.com/i915: 1
memory: 4162Mi
volumeMounts:
- mountPath: /config
name: jellyfin-config
- mountPath: /data
name: jellyfin-data
- mountPath: /config/data/transcodes
name: transcodes
priorityClassName: tier-2
securityContext:
fsGroup: 104
seccompProfile:
type: RuntimeDefault
volumes:
- name: jellyfin-config
persistentVolumeClaim:
claimName: jellyfin-config-v2
- name: jellyfin-data
persistentVolumeClaim:
claimName: jellyfin-truenas-nfs-videos
- name: transcodes
persistentVolumeClaim:
claimName: jellyfin-transcodes