Files
argocd-apps/apps/base/jellyfin/statefulset.yaml
T
Ben Vincent ffcf646d87
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline failed
Rework jellyfin-ha into a true-HA StatefulSet deployment
Turn the single-replica jellyfin-ha app into a proper high-availability
deployment so the fork's Redis-coordinated distributed transcoding and
PostgreSQL main database can actually be exercised.

- Replace the Deployment with a 2-replica StatefulSet for stable pod
  identity; set JELLYFIN_INSTANCE_ID from metadata.name (the fork's Redis
  transcode-lease owner id), add soft podAntiAffinity and a PDB
  minAvailable 1.
- Move the main Jellyfin DB to PostgreSQL via a CloudNativePG trio
  (3-instance Cluster, PgBouncer Pooler, Ceph RGW barman backups) mirroring
  the litellm pattern; an init container writes database.xml selecting the
  fork's Jellyfin-PostgreSQL provider and the DSN is composed from the
  CNPG-generated app secret pointed at the pooler.
- Share /config on an RWX cephfs PVC across replicas; keep /cache per-pod
  via a volumeClaimTemplate.
- Fix the transcode mount to the fork's real path /config/transcodes on the
  RWX PVC (raid5) so a surviving pod can resume the segments of the pod it
  takes over.
- Add Intel iGPU hardware transcoding via the gpu.intel.com/i915 device
  plugin resource plus render/video supplemental groups.
- Switch the Service to sessionAffinity ClientIP to reduce transcode churn.
- Disable UDP auto-discovery. Library scans still run on every replica; a
  single-scanner leader election is a planned follow-up.
2026-08-10 23:27:36 +10:00

200 lines
7.2 KiB
YAML

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: jellyfin
namespace: jellyfin
spec:
# HA: two replicas coordinate transcode session ownership through Redis and
# resume each other's HLS segments off the shared RWX transcode PVC. Stable
# pod names (jellyfin-0/1) are the lease owner identity, hence StatefulSet.
replicas: 2
serviceName: jellyfin
podManagementPolicy: Parallel
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
app: jellyfin
template:
metadata:
labels:
app: jellyfin
spec:
securityContext:
# Group-write the shared RWX volumes and grant the render/video groups so
# the runAsUser 1000 process can open the Intel DRI render node injected
# by the device plugin.
fsGroup: 1000
supplementalGroups:
- 44
- 105
- 109
seccompProfile:
type: RuntimeDefault
affinity:
# Spread the two replicas across nodes for node-level HA. Soft so a
# single-GPU-node cluster still schedules both (i915 has 4 shared slots).
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: jellyfin
topologyKey: kubernetes.io/hostname
initContainers:
# Select the fork's experimental PostgreSQL provider by writing
# database.xml before Jellyfin starts. Runs as root to chown into the
# shared config volume; mirrors the fork Helm chart's inject-db-config.
- name: inject-db-config
image: busybox:1.37.0
command:
- sh
- -c
- |
mkdir -p /config/config
chown 1000:1000 /config/config
chmod 775 /config/config
cat > /config/config/database.xml << 'DBEOF'
<?xml version="1.0" encoding="utf-8"?>
<DatabaseConfigurationOptions>
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
<LockingBehavior>NoLock</LockingBehavior>
</DatabaseConfigurationOptions>
DBEOF
chown 1000:1000 /config/config/database.xml
chmod 664 /config/config/database.xml
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: config
mountPath: /config
containers:
- name: jellyfin
image: git.unkin.net/unkin/jellyfin-ha:v0.1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8096
protocol: TCP
env:
# Pod identity for the Redis transcode lease owner. The fork reads
# JELLYFIN_INSTANCE_ID (falling back to MachineName); the stable
# StatefulSet pod name gives each replica a unique lease identity so
# takeover can target a dead replica. JELLYFIN_HA_POD_NAME is set for
# parity with the fork Helm chart (nothing currently reads it).
- name: JELLYFIN_INSTANCE_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: JELLYFIN_HA_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
# Multiple replicas must not each answer UDP auto-discovery.
- name: JELLYFIN_Network__AutoDiscovery
value: "false"
# Config dir must differ from the data root (Jellyfin sanity check).
- name: JELLYFIN_CONFIG_DIR
value: /config/config
# Distributed transcode session store (jellyfin-ha additions).
- name: Jellyfin__TranscodeStore__RedisConnectionString
value: "redis:6379,abortConnect=false"
- name: Jellyfin__TranscodeStore__LeaseDurationSeconds
value: "30"
# PostgreSQL main DB via the CNPG-generated app secret, routed through
# the PgBouncer pooler. Composed with $(VAR) expansion so the password
# is never rendered into the manifest; CNPG passwords are URL-safe.
- name: PGUSER
valueFrom:
secretKeyRef:
name: jellyfin-postgres-app
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: jellyfin-postgres-app
key: password
- name: PGDB
valueFrom:
secretKeyRef:
name: jellyfin-postgres-app
key: dbname
- name: POSTGRES_CONNECTION_STRING
value: "postgresql://$(PGUSER):$(PGPASSWORD)@jellyfin-postgres-pooler:5432/$(PGDB)"
- name: DATABASE_URL
value: "postgresql://$(PGUSER):$(PGPASSWORD)@jellyfin-postgres-pooler:5432/$(PGDB)"
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: "1"
memory: 1Gi
gpu.intel.com/i915: "1"
limits:
cpu: "4"
memory: 6Gi
# Intel iGPU (QSV/VA-API) slot. Requesting it pins the pod to a
# GPU-labelled node and injects /dev/dri/renderD* automatically, so
# no /dev/dri hostPath or privileged container is needed. Enable
# QSV/VA-API once in the Jellyfin admin UI; it persists to /config.
gpu.intel.com/i915: "1"
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- name: config
mountPath: /config
- name: transcode
# Fork's real transcode temp path. RWX so a surviving pod reads the
# in-flight .ts/.m3u8 segments of the pod it takes over. A per-pod
# volume here silently breaks HA takeover.
mountPath: /config/transcodes
- name: cache
mountPath: /cache
- name: media
mountPath: /media
readOnly: true
volumes:
- name: config
persistentVolumeClaim:
claimName: jellyfin-config
- name: transcode
persistentVolumeClaim:
claimName: jellyfin-transcode
- name: media
persistentVolumeClaim:
claimName: jellyfin-media
volumeClaimTemplates:
# Per-pod scratch cache — RWO, disposable, one PVC per replica.
- metadata:
name: cache
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 30Gi
storageClassName: cephrbd-fast-delete
volumeMode: Filesystem