ffcf646d87
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.
200 lines
7.2 KiB
YAML
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
|