feat: Add Helm chart for Jellyfin media server with PostgreSQL support.

This commit is contained in:
sha
2026-03-16 00:33:42 +02:00
commit 2f2786adba
9 changed files with 510 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "jellyfin.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.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 name and version as used by the chart label.
*/}}
{{- define "jellyfin.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "jellyfin.labels" -}}
helm.sh/chart: {{ include "jellyfin.chart" . }}
{{ include "jellyfin.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "jellyfin.selectorLabels" -}}
app.kubernetes.io/name: {{ include "jellyfin.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
+47
View File
@@ -0,0 +1,47 @@
{{- if .Values.backup.enabled -}}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "jellyfin.fullname" . }}-backup
{{- if .Values.namespaceOverride }}
namespace: {{ .Values.namespaceOverride }}
{{- end }}
labels:
{{- include "jellyfin.labels" . | nindent 4 }}
spec:
schedule: {{ .Values.backup.schedule | quote }}
jobTemplate:
spec:
template:
spec:
containers:
- name: postgres-backup
image: {{ .Values.backup.image }}
command:
- /bin/sh
- -c
- |
# Backup Jellyfin database
pg_dump -h {{ .Values.postgres.host }} -U {{ .Values.postgres.user }} -d $DB_NAME | gzip > /backup/$DB_NAME-$(date +%Y%m%d-%H%M%S).sql.gz
# Retention: Delete backups older than specified days
find /backup -name "$DB_NAME-*.sql.gz" -mtime +$RETENTION_DAYS -delete
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.postgres.existingSecret }}
key: JELLYFIN_POSTGRESQL_PASSWORD
- name: DB_NAME
value: {{ .Values.postgres.db | quote }}
- name: RETENTION_DAYS
value: {{ .Values.backup.retentionDays | quote }}
volumeMounts:
- name: backup-storage
mountPath: /backup
volumes:
- name: backup-storage
hostPath:
path: {{ .Values.backup.storagePath }}
type: DirectoryOrCreate
restartPolicy: OnFailure
{{- end }}
+94
View File
@@ -0,0 +1,94 @@
{{- if .Values.dbInit.enabled -}}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "jellyfin.fullname" . }}-db-init
{{- if .Values.namespaceOverride }}
namespace: {{ .Values.namespaceOverride }}
{{- end }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-delete-policy": before-hook-creation
labels:
{{- include "jellyfin.labels" . | nindent 4 }}
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: db-init
image: {{ .Values.dbInit.image }}
command:
- /bin/sh
- -c
- |
set -ex
# Set admin user from secret or values
if [ -n "$ADMIN_USER_FROM_SECRET" ]; then
PGUSER=$ADMIN_USER_FROM_SECRET
else
PGUSER={{ .Values.postgres.adminUser | quote }}
fi
export PGUSER
# Wait for PostgreSQL to be ready
until pg_isready -h $PGHOST -p $PGPORT -U $PGUSER; do
echo "Waiting for PostgreSQL to be ready..."
sleep 2
done
# Connect as admin user to create the jellyfin user and database
export PGPASSWORD=$POSTGRES_ADMIN_PASSWORD
# Create or update database user password
echo "Ensuring $POSTGRES_USER user exists with correct password..."
if psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='$POSTGRES_USER'" | grep -q 1; then
psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "ALTER USER \"$POSTGRES_USER\" WITH PASSWORD '${JELLYFIN_PASSWORD}';"
else
psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "CREATE USER \"$POSTGRES_USER\" WITH PASSWORD '${JELLYFIN_PASSWORD}';"
fi
# Create Jellyfin database if it doesn't exist
echo "Creating $POSTGRES_DB database..."
psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB'" | grep -q 1 || \
psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "CREATE DATABASE \"$POSTGRES_DB\" OWNER \"$POSTGRES_USER\";"
# Grant all privileges to jellyfin user
echo "Granting privileges..."
psql -h $PGHOST -p $PGPORT -U $PGUSER -d "$POSTGRES_DB" -c "GRANT ALL PRIVILEGES ON DATABASE \"$POSTGRES_DB\" TO \"$POSTGRES_USER\";"
psql -h $PGHOST -p $PGPORT -U $PGUSER -d "$POSTGRES_DB" -c "GRANT ALL PRIVILEGES ON SCHEMA public TO \"$POSTGRES_USER\";"
psql -h $PGHOST -p $PGPORT -U $PGUSER -d "$POSTGRES_DB" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"$POSTGRES_USER\";"
psql -h $PGHOST -p $PGPORT -U $PGUSER -d "$POSTGRES_DB" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"$POSTGRES_USER\";"
echo "Database initialization complete."
env:
- name: PGHOST
value: {{ .Values.postgres.host | quote }}
- name: PGPORT
value: {{ .Values.postgres.port | quote }}
- name: ADMIN_USER
value: {{ .Values.postgres.adminUser | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgres.db | quote }}
- name: POSTGRES_USER
value: {{ .Values.postgres.user | quote }}
- name: POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.postgres.adminSecret | default .Values.postgres.existingSecret }}
key: {{ .Values.postgres.adminSecretKey }}
{{- if .Values.postgres.adminUserKey }}
- name: ADMIN_USER_FROM_SECRET
valueFrom:
secretKeyRef:
name: {{ .Values.postgres.adminSecret | default .Values.postgres.existingSecret }}
key: {{ .Values.postgres.adminUserKey }}
{{- end }}
- name: JELLYFIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.postgres.existingSecret }}
key: JELLYFIN_POSTGRESQL_PASSWORD
resources:
{{- toYaml .Values.dbInit.resources | nindent 12 }}
{{- end }}
+102
View File
@@ -0,0 +1,102 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "jellyfin.fullname" . }}
{{- if .Values.namespaceOverride }}
namespace: {{ .Values.namespaceOverride }}
{{- end }}
labels:
{{- include "jellyfin.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "jellyfin.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "jellyfin.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8096
protocol: TCP
env:
- name: POSTGRES_HOST
value: {{ .Values.postgres.host | quote }}
- name: POSTGRES_PORT
value: {{ .Values.postgres.port | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgres.db | quote }}
- name: POSTGRES_USER
value: {{ .Values.postgres.user | quote }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.postgres.existingSecret }}
key: JELLYFIN_POSTGRESQL_PASSWORD
volumeMounts:
{{- if .Values.persistence.media.enabled }}
- name: media
mountPath: {{ .Values.persistence.media.mountPath }}
{{- end }}
{{- if .Values.persistence.config.enabled }}
- name: config
mountPath: {{ .Values.persistence.config.mountPath }}
{{- end }}
{{- if .Values.persistence.dri.enabled }}
- name: dri
mountPath: {{ .Values.persistence.dri.mountPath }}
{{- end }}
{{- if .Values.persistence.extraVolumeMounts }}
{{- toYaml .Values.persistence.extraVolumeMounts | nindent 12 }}
{{- end }}
securityContext:
privileged: true
capabilities:
add: ["SYS_ADMIN"]
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
{{- if .Values.persistence.media.enabled }}
- name: media
hostPath:
path: {{ .Values.persistence.media.hostPath }}
type: DirectoryOrCreate
{{- end }}
{{- if .Values.persistence.config.enabled }}
- name: config
{{- if eq .Values.persistence.config.type "hostPath" }}
hostPath:
path: {{ .Values.persistence.config.hostPath }}
type: DirectoryOrCreate
{{- else }}
persistentVolumeClaim:
claimName: {{ .Values.persistence.config.existingClaim | default (include "jellyfin.fullname" .) }}
{{- end }}
{{- end }}
{{- if .Values.persistence.dri.enabled }}
- name: dri
hostPath:
path: {{ .Values.persistence.dri.hostPath }}
type: Directory
{{- end }}
{{- if .Values.persistence.extraVolumes }}
{{- toYaml .Values.persistence.extraVolumes | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+53
View File
@@ -0,0 +1,53 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := (include "jellyfin.fullname" .) -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className }}
{{- end }}
{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
{{- if .Values.namespaceOverride }}
namespace: {{ .Values.namespaceOverride }}
{{- end }}
labels:
{{- (include "jellyfin.labels" .) | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
+24
View File
@@ -0,0 +1,24 @@
{{- if .Values.persistence.config.enabled }}
{{- if eq .Values.persistence.config.type "pvc" }}
{{- if not .Values.persistence.config.existingClaim }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "jellyfin.fullname" . }}
{{- if .Values.namespaceOverride }}
namespace: {{ .Values.namespaceOverride }}
{{- end }}
labels:
{{- include "jellyfin.labels" . | nindent 4 }}
spec:
accessModes:
{{- toYaml .Values.persistence.config.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.persistence.config.size | quote }}
{{- if .Values.persistence.config.storageClass }}
storageClassName: {{ .Values.persistence.config.storageClass | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "jellyfin.fullname" . }}
{{- if .Values.namespaceOverride }}
namespace: {{ .Values.namespaceOverride }}
{{- end }}
labels:
{{- include "jellyfin.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: 8096
protocol: TCP
name: http
selector:
{{- include "jellyfin.selectorLabels" . | nindent 4 }}