Deploy jellyfin-ha as a true-HA StatefulSet under a new media project #237
Reference in New Issue
Block a user
Delete Branch "benvin/jellyfin-ha"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Deploy the jellyfin-ha fork as a genuine high-availability service rather than a
single replica, so its two headline capabilities can actually be exercised: the
Redis-coordinated distributed transcoding (a surviving pod resumes the HLS
segments of a pod that dies mid-stream) and the experimental PostgreSQL main
database (which is what lets more than one replica share the same library). It
lands in its own
jellyfinnamespace under a newmediaArgoCD project.How
Workload — StatefulSet, 2 replicas. The Deployment becomes a StatefulSet so
each pod has a stable name. The fork's Redis transcode lease keys ownership on
JELLYFIN_INSTANCE_ID, which is set from the downward-API pod name, giving eachreplica a unique, stable lease identity. Soft
podAntiAffinityspreads the twopods across nodes and a
PodDisruptionBudgetkeepsminAvailable: 1throughdrains and rollouts.
Main database — CloudNativePG. A CNPG trio in-namespace mirrors the litellm
pattern: a 3-instance
Cluster, a PgBouncerPooler, and Ceph RGW (barman) S3backups to a dedicated
cnpg-jellyfinbucket owned by acephrgwObjectStoreUser. An init container writes/config/config/database.xmlselecting the fork's
Jellyfin-PostgreSQLprovider, and the connection string iscomposed from the CNPG-generated
jellyfin-postgres-appsecret (username /password / dbname) pointed at the pooler service — the password is never
rendered into the manifest. Library-item metadata therefore moves off SQLite;
metadata images, plugins, subtitles and config XML stay on
/config.Storage.
/configis now a sharedReadWriteManycephfs PVC (raid5, retain) so everyreplica reads/writes the same metadata and config.
/config/transcodes— the fork's real transcode temp path — is a shared RWXPVC (raid5, delete). This is the load-bearing fix: takeover reads the dead
pod's in-flight
.ts/.m3u8segments off shared storage, so per-pod scratchwould silently break it.
/cacheis per-pod via avolumeClaimTemplate(RWO).it is out of scope.
Hardware transcoding. The container requests the
gpu.intel.com/i915Inteldevice-plugin resource (which pins the pod to a GPU-labelled node and injects the
DRI render node — no
/dev/drihostPath or privileged container) plus therender/video supplemental groups. VA-API hardware acceleration is now on by
default: the
inject-configinit container seeds/config/config/encoding.xmlwith
HardwareAccelerationTypevaapi,EnableHardwareEncoding, the injectedrender node (
/dev/dri/renderD128) and h264/hevc hardware decode, so transcodesuse the iGPU on first boot with no manual admin-UI step. Both seed files
(
database.xml,encoding.xml) are written only when absent, so later adminchanges persisted to the shared RWX
/configare never clobbered on restart.Networking. The Gateway/HTTPRoute (traefik-internal,
jellyfin.k8s.syd1.au.unkin.net) is unchanged; the Service gainssessionAffinity: ClientIPto keep a client pinned to one replica and reducetranscode-session churn.
Redis. The in-namespace single-replica Redis stays as the transcode lease
store.
Follow-up
UDP auto-discovery is disabled, but scheduled library scans still run on every
replica (redundant scans). Single-scanner leader election is a planned follow-up
pending a fork feature, tracked separately.
c9c76f250btoffcf646d87Deploy jellyfin-ha under a new media projectto Deploy jellyfin-ha as a true-HA StatefulSet under a new media projectffcf646d87tof89e1c8260Review —
kustomize build --enable-helm apps/overlays/au-syd1/jellyfinrenders cleanly (5 PVCs, 2 Buckets + 1 ObjectStoreUser + 1 BucketAccess, valid k8up Schedule). vault-ca-cert is auto-reflected into every namespace (certificates/vault-ca-cert.yaml hasreflection-auto-enabled: true, empty allowed-namespaces), so both the CNPGendpointCAand the Schedule mount resolve in the jellyfin ns. k8up Schedule TLS/volume wiring, S3 cred keys, bucket ref, restic secret ref and crons all check out. Media PVC split, RO /media mounts, and no danglingjellyfin-mediaref confirmed.One substantive issue:
apps/base/jellyfin/schedule.yaml— k8up will back up the whole namespace, not just jellyfin-config. A k8up Schedule/Backup has no per-PVC selector; it scans the namespace and (with the vendored chart defaultskipWithoutAnnotation: false, k8up-4.10.0/k8up/values.yaml:57) backs up every PVC unless it carriesk8up.io/backup: "false". None of these PVCs have that annotation, so the nightly restic job will also pulljellyfin-media-tv(500Gi),jellyfin-media-movies(500Gi),jellyfin-transcode(100Gi scratch),jellyfin-redis-data, and the per-podcachePVCs into the config-backup repo — contrary to the stated "backups of the jellyfin-config PVC" intent. The RWO ones (cache/redis) will additionally fail to mount into the backup pod when in use, causing failed jobs.Fix: annotate
jellyfin-transcode,jellyfin-media-tv,jellyfin-media-movies,jellyfin-redis-data(and the StatefulSetcachevolumeClaimTemplate) withk8up.io/backup: "false"so onlyjellyfin-configis captured.Merge-order note (expected, not a defect): the k8up runner image (ghcr.io/k8up-io/k8up) needs the terraform-artifactapi allowlist PR merged+applied before backup jobs can pull.