diff --git a/deploy/helm/jellyfin-ha/Chart.yaml b/deploy/helm/jellyfin-ha/Chart.yaml new file mode 100644 index 000000000..85388087c --- /dev/null +++ b/deploy/helm/jellyfin-ha/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: jellyfin-ha +description: > + High-availability Jellyfin media server with Redis-backed transcode session + store, lease-aware segment cleanup, and optional PostgreSQL database provider + for multi-pod Kubernetes deployments. +type: application +version: 0.1.0 +appVersion: "10.12.0" +keywords: + - jellyfin + - media-server + - high-availability + - redis + - kubernetes +home: https://github.com/ZoltyMat/jellyfin-ha +sources: + - https://github.com/ZoltyMat/jellyfin-ha +maintainers: + - name: ZoltyMat + url: https://github.com/ZoltyMat +icon: https://raw.githubusercontent.com/jellyfin/jellyfin/master/Jellyfin.Server/Resources/Images/jellyfin-icon-solid.png diff --git a/deploy/helm/jellyfin-ha/templates/NOTES.txt b/deploy/helm/jellyfin-ha/templates/NOTES.txt new file mode 100644 index 000000000..d97893746 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/NOTES.txt @@ -0,0 +1,49 @@ +1. Jellyfin HA has been deployed. + +{{- if eq (int .Values.replicaCount) 1 }} +⚠ replicaCount=1 — running in single-instance mode. Set replicaCount >= 2 and + ha.enabled=true to enable HA transcoding. +{{- else }} +✔ Running {{ .Values.replicaCount }} replicas. +{{- if include "jellyfin-ha.haEnabled" . }} +✔ HA mode: ACTIVE — transcode sessions replicated via Redis. +{{- else }} +⚠ HA mode: INACTIVE — NullTranscodeSessionStore in use. + Set redis.enabled=true (or ha.transcodeStore.redisConnectionString) to enable HA. +{{- end }} +{{- end }} + +2. Get the Jellyfin URL: + +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + https://{{ .host }}/ +{{- end }} +{{- else if .Values.traefikIngressRoute.enabled }} + https://{{ .Values.traefikIngressRoute.host }}/ +{{- else }} + Access via port-forward: + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "jellyfin-ha.fullname" . }} 8096:{{ .Values.service.port }} + http://localhost:8096/ +{{- end }} + +3. PostgreSQL: +{{- if .Values.postgresql.enabled }} +✔ In-cluster PostgreSQL deployed. Ensure the secret "{{ .Values.postgresql.existingSecret }}" + exists in namespace {{ .Release.Namespace }} before starting the server. +{{- else if eq .Values.config.databaseType "Jellyfin-PostgreSQL" }} +⚠ config.databaseType=Jellyfin-PostgreSQL but postgresql.enabled=false. + Make sure you have an external PostgreSQL and the correct DATABASE_URL env var set. +{{- else }} + Using SQLite (default). Enable postgresql.enabled=true for a shared database backend. +{{- end }} + +4. Transcode storage: + The transcode PVC must be ReadWriteMany when replicaCount > 1. + Current accessMode: {{ .Values.persistence.transcode.accessMode }} +{{- if and (gt (int .Values.replicaCount) 1) (ne .Values.persistence.transcode.accessMode "ReadWriteMany") }} + +⚠ WARNING: replicaCount > 1 but transcode accessMode is not ReadWriteMany. + Pod B cannot read Pod A's HLS segments during session takeover. + Set persistence.transcode.accessMode=ReadWriteMany or use an NFS / Longhorn RWX PVC. +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/_helpers.tpl b/deploy/helm/jellyfin-ha/templates/_helpers.tpl new file mode 100644 index 000000000..fbac282a4 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/_helpers.tpl @@ -0,0 +1,137 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "jellyfin-ha.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "jellyfin-ha.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart label. +*/}} +{{- define "jellyfin-ha.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "jellyfin-ha.labels" -}} +helm.sh/chart: {{ include "jellyfin-ha.chart" . }} +{{ include "jellyfin-ha.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "jellyfin-ha.selectorLabels" -}} +app.kubernetes.io/name: {{ include "jellyfin-ha.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: server +{{- end }} + +{{/* +Service account name. +*/}} +{{- define "jellyfin-ha.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "jellyfin-ha.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Fully qualified name of the in-cluster Redis service. +*/}} +{{- define "jellyfin-ha.redis.fullname" -}} +{{- printf "%s-redis" (include "jellyfin-ha.fullname" .) }} +{{- end }} + +{{/* +Fully qualified name of the in-cluster PostgreSQL service. +*/}} +{{- define "jellyfin-ha.postgres.fullname" -}} +{{- printf "%s-postgres" (include "jellyfin-ha.fullname" .) }} +{{- end }} + +{{/* +Compute the Redis connection string. +Priority: + 1. existingSecret (mounted as env var in the statefulset template) + 2. explicit ha.transcodeStore.redisConnectionString value + 3. auto-compose from the in-cluster Redis service name when redis.enabled=true +Returns empty string if none of the above apply (= single-instance / NullStore mode). +This helper returns the literal string only for cases 2 and 3; case 1 is handled +directly in the container env block via secretKeyRef. +*/}} +{{- define "jellyfin-ha.redisConnectionString" -}} +{{- if .Values.ha.transcodeStore.redisConnectionString }} +{{- .Values.ha.transcodeStore.redisConnectionString }} +{{- else if .Values.redis.enabled }} +{{- printf "%s:6379,abortConnect=false" (include "jellyfin-ha.redis.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Return true if HA mode is active and Redis should be wired up. +*/}} +{{- define "jellyfin-ha.haEnabled" -}} +{{- if and .Values.ha.enabled (or .Values.redis.enabled .Values.ha.transcodeStore.redisConnectionString .Values.ha.transcodeStore.existingSecret) }} +{{- "true" }} +{{- end }} +{{- end }} + +{{/* +Config PVC claim name — either the existing claim or the chart-managed one. +*/}} +{{- define "jellyfin-ha.configPvcName" -}} +{{- if .Values.persistence.config.existingClaim }} +{{- .Values.persistence.config.existingClaim }} +{{- else }} +{{- printf "%s-config" (include "jellyfin-ha.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Transcode PVC claim name — either the existing claim or the chart-managed one. +*/}} +{{- define "jellyfin-ha.transcodePvcName" -}} +{{- if .Values.persistence.transcode.existingClaim }} +{{- .Values.persistence.transcode.existingClaim }} +{{- else }} +{{- printf "%s-transcode" (include "jellyfin-ha.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Media PVC claim name — either the existing claim or the chart-managed NFS PVC. +*/}} +{{- define "jellyfin-ha.mediaPvcName" -}} +{{- if .Values.persistence.media.existingClaim }} +{{- .Values.persistence.media.existingClaim }} +{{- else if .Values.persistence.media.nfs.enabled }} +{{- printf "%s-media" (include "jellyfin-ha.fullname" .) }} +{{- end }} +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/configmap-runtimeconfig.yaml b/deploy/helm/jellyfin-ha/templates/configmap-runtimeconfig.yaml new file mode 100644 index 000000000..4cec56144 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/configmap-runtimeconfig.yaml @@ -0,0 +1,15 @@ +{{- if .Values.runtimeConfig.enabled }} +# jellyfin.runtimeconfig.json ConfigMap. +# Mount path: /jellyfin/jellyfin.runtimeconfig.json +# Use this to set .NET runtime configuration switches (e.g. Intel QSV codec flags). +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "jellyfin-ha.fullname" . }}-runtimeconfig + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} +data: + jellyfin.runtimeconfig.json: | + {{- .Values.runtimeConfig.json | nindent 4 }} +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/ingress.yaml b/deploy/helm/jellyfin-ha/templates/ingress.yaml new file mode 100644 index 000000000..923cec7a2 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/ingress.yaml @@ -0,0 +1,97 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "jellyfin-ha.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "jellyfin-ha.fullname" $ }} + port: + name: http + {{- end }} + {{- end }} +{{- end }} + +--- +{{- if .Values.traefikIngressRoute.enabled }} +# Traefik v3 IngressRoute (used by k3s default ingress controller). +# Enables sticky session cookies — required for multi-replica Jellyfin so that +# a client always lands on the same pod (session affinity). +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: {{ include "jellyfin-ha.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + {{- if .Values.traefikIngressRoute.tls.enabled }} + annotations: + cert-manager.io/cluster-issuer: {{ .Values.traefikIngressRoute.tls.clusterIssuer }} + {{- end }} +spec: + entryPoints: + {{- toYaml .Values.traefikIngressRoute.entryPoints | nindent 4 }} + routes: + - match: Host(`{{ .Values.traefikIngressRoute.host }}`) + kind: Rule + services: + - name: {{ include "jellyfin-ha.fullname" . }} + port: {{ .Values.service.port }} + {{- if .Values.traefikIngressRoute.sticky.enabled }} + sticky: + cookie: + name: {{ .Values.traefikIngressRoute.sticky.cookieName }} + httpOnly: {{ .Values.traefikIngressRoute.sticky.httpOnly }} + secure: {{ .Values.traefikIngressRoute.sticky.secure }} + {{- end }} + {{- if .Values.traefikIngressRoute.tls.enabled }} + tls: + secretName: {{ .Values.traefikIngressRoute.tls.secretName }} + {{- end }} + +--- +{{- if .Values.traefikIngressRoute.tls.enabled }} +# cert-manager Certificate for Traefik TLS termination. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "jellyfin-ha.fullname" . }}-tls + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} +spec: + secretName: {{ .Values.traefikIngressRoute.tls.secretName }} + issuerRef: + name: {{ .Values.traefikIngressRoute.tls.clusterIssuer }} + kind: ClusterIssuer + dnsNames: + {{- if .Values.traefikIngressRoute.tls.dnsNames }} + {{- toYaml .Values.traefikIngressRoute.tls.dnsNames | nindent 4 }} + {{- else }} + - {{ .Values.traefikIngressRoute.host }} + {{- end }} +{{- end }} +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/pdb.yaml b/deploy/helm/jellyfin-ha/templates/pdb.yaml new file mode 100644 index 000000000..81278d61f --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/pdb.yaml @@ -0,0 +1,14 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "jellyfin-ha.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} +spec: + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + selector: + matchLabels: + {{- include "jellyfin-ha.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/postgres-pvc.yaml b/deploy/helm/jellyfin-ha/templates/postgres-pvc.yaml new file mode 100644 index 000000000..582eee246 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/postgres-pvc.yaml @@ -0,0 +1,20 @@ +{{- if .Values.postgresql.enabled }} +# PersistentVolumeClaim for the in-cluster PostgreSQL data directory. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "jellyfin-ha.postgres.fullname" . }}-data + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.postgresql.persistence.storageClass }} + storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgresql.persistence.size }} +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/postgres-service.yaml b/deploy/helm/jellyfin-ha/templates/postgres-service.yaml new file mode 100644 index 000000000..06a8eee94 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/postgres-service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.postgresql.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "jellyfin-ha.postgres.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: {{ include "jellyfin-ha.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: database + ports: + - name: postgres + port: {{ .Values.postgresql.service.port }} + targetPort: postgres + protocol: TCP +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/postgres-statefulset.yaml b/deploy/helm/jellyfin-ha/templates/postgres-statefulset.yaml new file mode 100644 index 000000000..4d92a261c --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/postgres-statefulset.yaml @@ -0,0 +1,84 @@ +{{- if .Values.postgresql.enabled }} +# In-cluster PostgreSQL StatefulSet — experimental. +# The credentials secret must be created manually before first deploy: +# +# kubectl create secret generic {{ .Values.postgresql.existingSecret }} \ +# --namespace {{ .Release.Namespace }} \ +# --from-literal=POSTGRES_USER=jellyfin \ +# --from-literal=POSTGRES_PASSWORD= \ +# --from-literal=POSTGRES_DB=jellyfin \ +# --from-literal=DATABASE_URL="postgresql://jellyfin:@{{ include "jellyfin-ha.postgres.fullname" . }}:5432/jellyfin" +# +# SECURITY: Do NOT add a Secret resource here. Applying this file must not +# overwrite a live secret. +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "jellyfin-ha.postgres.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + replicas: 1 + serviceName: {{ include "jellyfin-ha.postgres.fullname" . }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "jellyfin-ha.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: database + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "jellyfin-ha.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: database + spec: + containers: + - name: postgres + image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}" + imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }} + ports: + - name: postgres + containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.existingSecret }} + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.existingSecret }} + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.existingSecret }} + key: POSTGRES_DB + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + resources: + {{- toYaml .Values.postgresql.resources | nindent 12 }} + livenessProbe: + exec: + command: ["pg_isready", "-U", "$(POSTGRES_USER)"] + initialDelaySeconds: 30 + periodSeconds: 20 + timeoutSeconds: 5 + readinessProbe: + exec: + command: ["pg_isready", "-U", "$(POSTGRES_USER)"] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "jellyfin-ha.postgres.fullname" . }}-data +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/pvc.yaml b/deploy/helm/jellyfin-ha/templates/pvc.yaml new file mode 100644 index 000000000..99611d17b --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/pvc.yaml @@ -0,0 +1,90 @@ +{{- if not .Values.persistence.config.existingClaim }} +# Shared config PVC — used by all Jellyfin replicas. +# Must be ReadWriteMany when replicaCount > 1. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "jellyfin-ha.fullname" . }}-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: storage +spec: + accessModes: + - {{ .Values.persistence.config.accessMode }} + {{- if .Values.persistence.config.storageClass }} + storageClassName: {{ .Values.persistence.config.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.config.size }} +{{- end }} + +--- +{{- if not .Values.persistence.transcode.existingClaim }} +# Shared transcode PVC — must be ReadWriteMany so pod takeover can read +# HLS segments written by the previous owner pod. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "jellyfin-ha.fullname" . }}-transcode + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: storage +spec: + accessModes: + - {{ .Values.persistence.transcode.accessMode }} + {{- if .Values.persistence.transcode.storageClass }} + storageClassName: {{ .Values.persistence.transcode.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.transcode.size }} +{{- end }} + +--- +{{- if and .Values.persistence.media.nfs.enabled (not .Values.persistence.media.existingClaim) }} +# NFS PersistentVolume and PersistentVolumeClaim for the media library. +# Enable persistence.media.nfs.enabled and provide server/path to use this. +# Alternatively, set persistence.media.existingClaim to reuse an existing PVC. +apiVersion: v1 +kind: PersistentVolume +metadata: + name: {{ include "jellyfin-ha.fullname" . }}-media-nfs + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: storage +spec: + capacity: + storage: {{ .Values.persistence.media.nfs.size }} + accessModes: + - ReadOnlyMany + persistentVolumeReclaimPolicy: Retain + {{- if .Values.persistence.media.nfs.storageClass }} + storageClassName: {{ .Values.persistence.media.nfs.storageClass | quote }} + {{- end }} + nfs: + server: {{ .Values.persistence.media.nfs.server | quote }} + path: {{ .Values.persistence.media.nfs.path | quote }} + readOnly: true +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "jellyfin-ha.fullname" . }}-media + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: storage +spec: + accessModes: + - ReadOnlyMany + {{- if .Values.persistence.media.nfs.storageClass }} + storageClassName: {{ .Values.persistence.media.nfs.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.media.nfs.size }} + volumeName: {{ include "jellyfin-ha.fullname" . }}-media-nfs +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/redis-configmap.yaml b/deploy/helm/jellyfin-ha/templates/redis-configmap.yaml new file mode 100644 index 000000000..face786c8 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/redis-configmap.yaml @@ -0,0 +1,15 @@ +{{- if .Values.redis.enabled }} +# ConfigMap holding the Redis configuration file. +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "jellyfin-ha.redis.fullname" . }}-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +data: + redis.conf: | + maxmemory {{ .Values.redis.maxmemory }} + maxmemory-policy {{ .Values.redis.maxmemoryPolicy }} +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/redis-deployment.yaml b/deploy/helm/jellyfin-ha/templates/redis-deployment.yaml new file mode 100644 index 000000000..d4cff7121 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/redis-deployment.yaml @@ -0,0 +1,58 @@ +{{- if .Values.redis.enabled }} +# In-cluster Redis Deployment for jellyifn-ha transcode session store. +# No persistence — lease data is small and reconstructable on restart. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "jellyfin-ha.redis.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: {{ include "jellyfin-ha.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: redis + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "jellyfin-ha.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: redis + spec: + containers: + - name: redis + image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}" + imagePullPolicy: {{ .Values.redis.image.pullPolicy }} + args: ["redis-server", "/etc/redis/redis.conf"] + ports: + - name: redis + containerPort: 6379 + protocol: TCP + resources: + {{- toYaml .Values.redis.resources | nindent 12 }} + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 5 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + volumeMounts: + - name: config + mountPath: /etc/redis + volumes: + - name: config + configMap: + name: {{ include "jellyfin-ha.redis.fullname" . }}-config +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/redis-service.yaml b/deploy/helm/jellyfin-ha/templates/redis-service.yaml new file mode 100644 index 000000000..56b12fb9e --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/redis-service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.redis.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "jellyfin-ha.redis.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: {{ include "jellyfin-ha.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: redis + ports: + - name: redis + port: 6379 + targetPort: redis + protocol: TCP +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/service.yaml b/deploy/helm/jellyfin-ha/templates/service.yaml new file mode 100644 index 000000000..49958a4dd --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "jellyfin-ha.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "jellyfin-ha.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/deploy/helm/jellyfin-ha/templates/servicemonitor.yaml b/deploy/helm/jellyfin-ha/templates/servicemonitor.yaml new file mode 100644 index 000000000..28ca727de --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/servicemonitor.yaml @@ -0,0 +1,27 @@ +{{- if .Values.serviceMonitor.enabled }} +# Prometheus ServiceMonitor. +# Jellyfin does not expose a native /metrics endpoint. Enable this if you have +# a Prometheus sidecar or plan to add one. The kube-state-metrics replica count +# alert is the primary health signal for Jellyfin without a native exporter. +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "jellyfin-ha.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + {{- with .Values.serviceMonitor.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "jellyfin-ha.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: {{ .Values.serviceMonitor.path }} + interval: {{ .Values.serviceMonitor.interval }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/helm/jellyfin-ha/templates/statefulset.yaml b/deploy/helm/jellyfin-ha/templates/statefulset.yaml new file mode 100644 index 000000000..866ad4b08 --- /dev/null +++ b/deploy/helm/jellyfin-ha/templates/statefulset.yaml @@ -0,0 +1,277 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "jellyfin-ha.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "jellyfin-ha.labels" . | nindent 4 }} + {{- with .Values.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicaCount }} + serviceName: {{ include "jellyfin-ha.fullname" . }} + updateStrategy: + {{- toYaml .Values.updateStrategy | nindent 4 }} + selector: + matchLabels: + {{- include "jellyfin-ha.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "jellyfin-ha.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "jellyfin-ha.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + + # --------------------------------------------------------------------------- + # Affinity / anti-affinity + # --------------------------------------------------------------------------- + affinity: + {{- if and .Values.gpu.enabled .Values.gpu.intel.nodeLabel.key }} + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: {{ .Values.gpu.intel.nodeLabel.key }} + operator: In + values: + - {{ .Values.gpu.intel.nodeLabel.value }} + {{- end }} + {{- if .Values.podAntiAffinity.enabled }} + podAntiAffinity: + {{- if eq .Values.podAntiAffinity.type "required" }} + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + {{- include "jellyfin-ha.selectorLabels" . | nindent 18 }} + topologyKey: kubernetes.io/hostname + {{- else }} + preferredDuringSchedulingIgnoredDuringExecution: + - weight: {{ .Values.podAntiAffinity.weight }} + podAffinityTerm: + labelSelector: + matchLabels: + {{- include "jellyfin-ha.selectorLabels" . | nindent 20 }} + topologyKey: kubernetes.io/hostname + {{- end }} + {{- end }} + + # GPU node toleration + {{- if .Values.gpu.enabled }} + tolerations: + - key: {{ .Values.gpu.intel.toleration.key }} + operator: Equal + value: {{ .Values.gpu.intel.toleration.value | quote }} + effect: {{ .Values.gpu.intel.toleration.effect }} + {{- end }} + + # --------------------------------------------------------------------------- + # Init containers + # --------------------------------------------------------------------------- + initContainers: + {{- if eq .Values.config.databaseType "Jellyfin-PostgreSQL" }} + # Inject database.xml to select the PostgreSQL provider at startup. + - name: inject-db-config + image: busybox:1.37.0 + command: + - sh + - -c + - | + mkdir -p /config/config + chown {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.runAsGroup }} /config/config + chmod 775 /config/config + cat > /config/config/database.xml << 'DBEOF' + + + Jellyfin-PostgreSQL + NoLock + + DBEOF + chown {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.runAsGroup }} /config/config/database.xml + chmod 664 /config/config/database.xml + echo "database.xml injected." + volumeMounts: + - name: config + mountPath: /config + {{- end }} + {{- with .Values.extraInitContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + + # --------------------------------------------------------------------------- + # Main container + # --------------------------------------------------------------------------- + containers: + - name: jellyfin + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8096 + protocol: TCP + env: + # Pod identity — used by the Redis transcode lease store to identify this replica. + - name: JELLYFIN_HA_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: JELLYFIN_INSTANCE_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + + # Disable UDP auto-discovery when running multiple replicas. + - name: JELLYFIN_Network__AutoDiscovery + value: {{ .Values.config.autoDiscovery | quote }} + + # Config directory (must differ from data root; see Jellyfin sanity check). + - name: JELLYFIN_CONFIG_DIR + value: {{ .Values.config.configDir | quote }} + + {{- if .Values.config.publishedServerUrl }} + - name: JELLYFIN_PublishedServerUrl + value: {{ .Values.config.publishedServerUrl | quote }} + {{- end }} + + # --------------------------------------------------------------------------- + # Redis (HA transcode session store) + # --------------------------------------------------------------------------- + {{- if include "jellyfin-ha.haEnabled" . }} + {{- if .Values.ha.transcodeStore.existingSecret }} + # Connection string sourced from an existing secret. + - name: Jellyfin__TranscodeStore__RedisConnectionString + valueFrom: + secretKeyRef: + name: {{ .Values.ha.transcodeStore.existingSecret }} + key: {{ .Values.ha.transcodeStore.existingSecretKey }} + {{- else }} + - name: Jellyfin__TranscodeStore__RedisConnectionString + value: {{ include "jellyfin-ha.redisConnectionString" . | quote }} + {{- end }} + - name: Jellyfin__TranscodeStore__LeaseDurationSeconds + value: {{ .Values.ha.transcodeStore.leaseDurationSeconds | quote }} + {{- end }} + + # --------------------------------------------------------------------------- + # PostgreSQL (experimental) + # --------------------------------------------------------------------------- + {{- if and .Values.postgresql.enabled (eq .Values.config.databaseType "Jellyfin-PostgreSQL") }} + - name: POSTGRES_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.existingSecret }} + key: DATABASE_URL + {{- end }} + + # --------------------------------------------------------------------------- + # Extra environment variables + # --------------------------------------------------------------------------- + {{- with .Values.config.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + + resources: + {{- toYaml .Values.resources | nindent 12 }} + + securityContext: + privileged: {{ if and .Values.gpu.enabled .Values.gpu.mountDri }}true{{ else }}{{ .Values.securityContext.privileged }}{{ end }} + runAsUser: {{ .Values.securityContext.runAsUser }} + runAsGroup: {{ .Values.securityContext.runAsGroup }} + + volumeMounts: + - name: config + mountPath: /config + {{- if or .Values.persistence.media.existingClaim (and .Values.persistence.media.nfs.enabled) }} + - name: media + mountPath: /media + readOnly: true + {{- end }} + - name: transcode + mountPath: /config/transcodes + - name: cache + mountPath: /cache + {{- if and .Values.gpu.enabled .Values.gpu.mountDri }} + - name: dri + mountPath: /dev/dri + {{- end }} + {{- if .Values.runtimeConfig.enabled }} + - name: runtimeconfig + mountPath: /jellyfin/jellyfin.runtimeconfig.json + subPath: jellyfin.runtimeconfig.json + readOnly: true + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + + # --------------------------------------------------------------------------- + # Volumes (static — shared across all pods) + # --------------------------------------------------------------------------- + volumes: + - name: config + persistentVolumeClaim: + claimName: {{ include "jellyfin-ha.configPvcName" . }} + - name: transcode + persistentVolumeClaim: + claimName: {{ include "jellyfin-ha.transcodePvcName" . }} + {{- if or .Values.persistence.media.existingClaim .Values.persistence.media.nfs.enabled }} + - name: media + persistentVolumeClaim: + claimName: {{ include "jellyfin-ha.mediaPvcName" . }} + {{- end }} + {{- if and .Values.gpu.enabled .Values.gpu.mountDri }} + - name: dri + hostPath: + path: /dev/dri + {{- end }} + {{- if .Values.runtimeConfig.enabled }} + - name: runtimeconfig + configMap: + name: {{ include "jellyfin-ha.fullname" . }}-runtimeconfig + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + + # --------------------------------------------------------------------------- + # Per-pod volumes via volumeClaimTemplates + # Cache is per-pod (RWO) — each replica has an independent transcoding cache, + # which avoids lock contention and is safe to lose on pod termination. + # --------------------------------------------------------------------------- + volumeClaimTemplates: + - metadata: + name: cache + labels: + {{- include "jellyfin-ha.labels" . | nindent 10 }} + spec: + accessModes: + - ReadWriteOnce + {{- if .Values.persistence.cache.storageClass }} + storageClassName: {{ .Values.persistence.cache.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.cache.size }} diff --git a/deploy/helm/jellyfin-ha/values.yaml b/deploy/helm/jellyfin-ha/values.yaml new file mode 100644 index 000000000..5e933f9d5 --- /dev/null +++ b/deploy/helm/jellyfin-ha/values.yaml @@ -0,0 +1,405 @@ +# Default values for jellyfin-ha. +# This is a YAML-formatted file. + +# -- Override the chart name. +nameOverride: "" +# -- Override the full resource name prefix. +fullnameOverride: "" + +# -- Number of Jellyfin replicas. +# Set >= 2 to use HA mode. When replicaCount > 1, ha.enabled should be true +# and a Redis connection must be configured (via redis.enabled or ha.transcodeStore.redisConnectionString). +replicaCount: 2 + +# -- Container image configuration. +image: + repository: "your-registry/jellyfin-ha" + tag: "latest" + pullPolicy: IfNotPresent + +# -- Image pull secrets (e.g. for private ECR registries). +# Example: +# - name: ecr-pull-secret +imagePullSecrets: [] + +# --------------------------------------------------------------------------- +# HA (High-Availability) configuration +# --------------------------------------------------------------------------- +ha: + # -- Enable HA mode. When true, a Redis connection string is required + # (either via redis.enabled or ha.transcodeStore.redisConnectionString). + # When false, NullTranscodeSessionStore is used and behavior is identical + # to upstream Jellyfin. + enabled: true + + transcodeStore: + # -- StackExchange.Redis connection string. + # Leave empty to auto-compose from the in-cluster Redis service when redis.enabled=true. + # Explicit examples: + # redis:6379 + # redis:6379,password=secret + # redis.example.com:6380,ssl=true,abortConnect=false + # sentinel-host:26379,serviceName=mymaster + redisConnectionString: "" + + # -- How long (seconds) a pod's transcode lease is valid before another pod may take over. + leaseDurationSeconds: 30 + + # -- Secret containing the Redis connection string. + # If set, the connection string is read from this secret instead of the value above. + # The secret must have a key named by existingSecret.key. + existingSecret: "" + existingSecretKey: "connection-string" + +# --------------------------------------------------------------------------- +# In-cluster Redis (for transcode session store) +# --------------------------------------------------------------------------- +redis: + # -- Deploy an in-cluster Redis instance. + # Disable and set ha.transcodeStore.redisConnectionString to use an external Redis. + enabled: true + + image: + repository: redis + tag: "7.4.2-alpine3.21" + pullPolicy: IfNotPresent + + # -- Maximum memory for Redis to use. + maxmemory: "256mb" + # -- LRU eviction policy when maxmemory is reached. + maxmemoryPolicy: "allkeys-lru" + + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi + +# --------------------------------------------------------------------------- +# Jellyfin application configuration +# --------------------------------------------------------------------------- +config: + # -- The externally-reachable URL Jellyfin reports to clients. + publishedServerUrl: "" + + # -- Disable UDP auto-discovery (port 7359). + # Recommended when running multiple replicas to prevent duplicate discovery responses. + autoDiscovery: false + + # -- Jellyfin config directory inside the container. + # Must differ from the data/root directory to pass Jellyfin's sanity check. + configDir: "/config/config" + + # -- Database provider: "SQLite" (default) or "Jellyfin-PostgreSQL" (experimental). + # When set to "Jellyfin-PostgreSQL", an init container will inject database.xml + # and the postgresql.enabled section (or an external connection string) must be configured. + databaseType: "SQLite" + + # -- Extra environment variables to set on the Jellyfin container. + # Example: + # - name: JELLYFIN_Network__BaseUrl + # value: "/jellyfin" + extraEnv: [] + +# --------------------------------------------------------------------------- +# PostgreSQL (experimental — only needed when config.databaseType = Jellyfin-PostgreSQL) +# --------------------------------------------------------------------------- +postgresql: + # -- Deploy an in-cluster PostgreSQL instance. + enabled: false + + image: + repository: postgres + tag: "16.6-alpine3.21" + pullPolicy: IfNotPresent + + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + + persistence: + storageClass: "" + size: 5Gi + + # -- Name of an existing secret with PostgreSQL credentials. + # Required when postgresql.enabled=true. The secret must contain: + # POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, DATABASE_URL + # Create it with: + # kubectl create secret generic jellyfin-postgres-credentials \ + # --from-literal=POSTGRES_USER=jellyfin \ + # --from-literal=POSTGRES_PASSWORD= \ + # --from-literal=POSTGRES_DB=jellyfin \ + # --from-literal=DATABASE_URL="postgresql://jellyfin:@:5432/jellyfin" + existingSecret: "jellyfin-postgres-credentials" + + service: + port: 5432 + +# --------------------------------------------------------------------------- +# GPU / hardware transcoding +# --------------------------------------------------------------------------- +gpu: + # -- Enable Intel QSV / VA-API hardware transcoding. + # Mounts /dev/dri from the host and sets the required security context. + enabled: false + + intel: + # -- Node affinity label to prefer GPU-capable nodes. + nodeLabel: + key: gpu + value: intel-uhd-630 + + # -- Toleration for the GPU node taint. + toleration: + key: gpu + value: "true" + effect: NoSchedule + + # -- Mount /dev/dri from the host (required for VA-API; implies privileged=true). + mountDri: true + +# --------------------------------------------------------------------------- +# Persistence +# --------------------------------------------------------------------------- +persistence: + # Config volume — single-writer; RWO is fine for single-replica deployments. + # For multi-replica: use an RWX storage class (e.g. Longhorn RWX, NFS) or + # point all pods at an existing shared PVC via existingClaim. + config: + # -- Size of the config PVC. + size: 5Gi + # -- Storage class. Leave empty to use the cluster default. + storageClass: "" + # -- Access mode. Use ReadWriteMany when replicaCount > 1 and sharing one PVC. + accessMode: ReadWriteMany + # -- Reuse an existing PVC. When set, no new PVC is created. + existingClaim: "" + + # Media volume — read-only mount shared by all pods. + # Configure one of: existingClaim (for an existing PVC), nfs (to create an NFS PV+PVC), + # or existingClaim pointing at a pre-created PVC. + media: + # -- Reuse an existing media PVC (most common for homelab NFS/Longhorn setups). + existingClaim: "" + # -- Create an NFS-backed PV and PVC for the media library. + nfs: + enabled: false + server: "your-nas.local" + path: "/media" + size: 1Ti + storageClass: "" + + # Transcode volume — MUST be ReadWriteMany when replicaCount > 1 so that + # a recovering pod can read HLS segments written by the pod it is replacing. + # When replicaCount=1, ReadWriteOnce is acceptable. + transcode: + size: 30Gi + storageClass: "" + accessMode: ReadWriteMany + existingClaim: "" + + # Per-pod cache volume — local to each pod; always RWO. + # Created via StatefulSet volumeClaimTemplates (one PVC per pod). + cache: + size: 30Gi + storageClass: "" + +# --------------------------------------------------------------------------- +# Service +# --------------------------------------------------------------------------- +service: + type: ClusterIP + port: 8096 + # -- Annotations for the Service resource. + annotations: {} + +# --------------------------------------------------------------------------- +# Ingress (standard Kubernetes Ingress) +# --------------------------------------------------------------------------- +ingress: + enabled: false + # -- Ingress class name (e.g. "nginx", "traefik"). + className: "" + annotations: {} + # cert-manager.io/cluster-issuer: letsencrypt-prod + # nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: jellyfin-tls + # hosts: + # - jellyfin.example.com + +# --------------------------------------------------------------------------- +# Traefik IngressRoute (Traefik v3 CRD — used by k3s default ingress) +# --------------------------------------------------------------------------- +traefikIngressRoute: + enabled: false + entryPoints: + - websecure + # -- Hostname for the Traefik routing rule. + host: "jellyfin.example.com" + # -- Enable sticky session cookie (recommended for multi-replica Jellyfin). + sticky: + enabled: true + cookieName: "jellyfin-server-id" + httpOnly: true + secure: true + # -- cert-manager Certificate resource for TLS. + tls: + enabled: false + secretName: "jellyfin-tls" + clusterIssuer: "letsencrypt-prod" + dnsNames: [] + # - jellyfin.example.com + +# --------------------------------------------------------------------------- +# Resource requests and limits for the Jellyfin container +# --------------------------------------------------------------------------- +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: "4" + memory: 4Gi + +# --------------------------------------------------------------------------- +# Liveness and readiness probes +# --------------------------------------------------------------------------- +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 + +# --------------------------------------------------------------------------- +# Security context +# --------------------------------------------------------------------------- +# Container-level security context. +securityContext: + # -- Set to true only when GPU passthrough via /dev/dri is required. + # privileged=true is required for DRM ioctls (VA-API). Omit (false) for + # software-only transcoding. + privileged: false + # -- UID for the Jellyfin process. Use 10010 to match the svc-jellyfin NAS account + # when NFS root_squash is enabled. + runAsUser: 1000 + runAsGroup: 1000 + +# Pod-level security context. +podSecurityContext: + # -- fsGroup ensures mounted volumes are group-writable. + fsGroup: 1000 + # -- Additional groups for /dev/dri access (video=44, render=109 or 991). + supplementalGroups: [] + # - 44 # video + # - 109 # render (legacy) + # - 991 # render (Debian 13 trixie) + seccompProfile: + type: RuntimeDefault + +# --------------------------------------------------------------------------- +# Service account +# --------------------------------------------------------------------------- +serviceAccount: + create: false + name: "" + annotations: {} + +# --------------------------------------------------------------------------- +# Pod Disruption Budget +# --------------------------------------------------------------------------- +podDisruptionBudget: + enabled: true + minAvailable: 1 + +# --------------------------------------------------------------------------- +# Pod anti-affinity (spread replicas across nodes for node-level HA) +# --------------------------------------------------------------------------- +podAntiAffinity: + enabled: true + # -- "preferred" won't block scheduling if nodes are insufficient. + # Use "required" to enforce strict cross-node placement. + type: preferred + weight: 100 + +# --------------------------------------------------------------------------- +# Prometheus ServiceMonitor +# Note: Jellyfin has no native /metrics endpoint. This ServiceMonitor is +# included for future use (e.g. if you add a sidecar exporter) or for +# blackbox-style readiness monitoring. Disable if not using kube-prometheus-stack. +# --------------------------------------------------------------------------- +serviceMonitor: + enabled: false + # -- Scrape interval. + interval: "30s" + # -- Scrape path (Jellyfin does not expose Prometheus metrics natively). + path: /metrics + # -- Additional labels to add to the ServiceMonitor (e.g. to match a Prometheus release label). + additionalLabels: {} + # release: kube-prometheus-stack + +# --------------------------------------------------------------------------- +# Runtime config (jellyfin.runtimeconfig.json) +# Set dotnet runtime switches here if needed. Leave empty for defaults. +# --------------------------------------------------------------------------- +runtimeConfig: + enabled: false + # -- Raw JSON content for jellyfin.runtimeconfig.json. + # See jellyfin-runtimeconfig ConfigMap in the existing manifests for an example. + json: | + { + "configProperties": {} + } + +# --------------------------------------------------------------------------- +# Extra Kubernetes resources +# --------------------------------------------------------------------------- +# -- Additional volumes to attach to the Jellyfin pod. +extraVolumes: [] +# - name: my-extra-config +# configMap: +# name: my-configmap + +# -- Additional volume mounts for the Jellyfin container. +extraVolumeMounts: [] +# - name: my-extra-config +# mountPath: /etc/my-config + +# -- Additional init containers. +extraInitContainers: [] + +# -- Annotations to add to the StatefulSet. +annotations: {} +# -- Annotations to add to individual pods. +podAnnotations: {} +# -- Labels to add to the StatefulSet. +labels: {} +# -- Labels to add to individual pods. +podLabels: {} + +# -- Update strategy for the StatefulSet. +updateStrategy: + type: RollingUpdate