From bba3a09cfe758ab272f87bed42fe3c6873838a1a Mon Sep 17 00:00:00 2001 From: bart Date: Sat, 7 Nov 2026 10:44:11 +0100 Subject: [PATCH] feat(jellyfin): add kubernetes manifests and migration scripts --- README.md | 19 +++++ cleanup_old_jellyfin.sh | 55 +++++++++++++ k8s/jellyfin/deployment.yaml | 133 ++++++++++++++++++++++++++++++++ k8s/jellyfin/kustomization.yaml | 7 ++ k8s/jellyfin/pvc.yaml | 15 ++++ k8s/jellyfin/service.yaml | 17 ++++ k8s/kustomization.yaml | 1 + migrate_jellyfin_to_cluster.sh | 119 ++++++++++++++++++++++++++++ rollback_jellyfin_migration.sh | 43 +++++++++++ 9 files changed, 409 insertions(+) create mode 100755 cleanup_old_jellyfin.sh create mode 100644 k8s/jellyfin/deployment.yaml create mode 100644 k8s/jellyfin/kustomization.yaml create mode 100644 k8s/jellyfin/pvc.yaml create mode 100644 k8s/jellyfin/service.yaml create mode 100755 migrate_jellyfin_to_cluster.sh create mode 100755 rollback_jellyfin_migration.sh diff --git a/README.md b/README.md index 13a5e73..b4b3a75 100644 --- a/README.md +++ b/README.md @@ -98,3 +98,22 @@ De torrent indexer proxy (`jackett`) draait als pod in de `htpc` namespace: # Oude host bestanden en service opruimen: ./cleanup_old_jackett.sh ``` + +### Jellyfin +De Jellyfin media server (`jellyfin`) draait als pod in de `htpc` namespace: + +- **Dataopslag**: PersistentVolumeClaim `jellyfin-config` (30Gi, `local-path` op `/datadisk/infra/pv`), gemigreerd vanuit `/etc/jellyfin` en `/var/lib/jellyfin` via init-container (inclusief alle databases, metadata en plugins). +- **Mediaopslag**: HostPath `/datadisk` gemount op `/datadisk` en `/mnt/mp3` gemount op `/mnt/mp3` zodat alle bibliotheekpaden en symlinks exact behouden blijven. +- **Hardware Transcoding**: Directe hardware-acceleratie via VA-API met AMD Phoenix2 iGPU (`/dev/dri` hostPath gemount in container met `privileged: true`). +- **Netwerk & Ingress**: Luistert op poort `8096` (ClusterIP + HostPort `8096`). De Apache reverse proxy routeert `https://thuus.bnhmeijer.nl/jellyfin` en `wss://.../jellyfin/socket` naadloos door naar `127.0.0.1:8096`. +- **Migratie, Terugrol & Opruimen**: + ```bash + # Migratie uitvoeren (maakt backup in /storage/backups/, stopt host service, synct data, start pod): + ./migrate_jellyfin_to_cluster.sh + + # Terugrollen naar standalone host service indien gewenst: + ./rollback_jellyfin_migration.sh + + # Oude host bestanden en service opruimen: + ./cleanup_old_jellyfin.sh + ``` diff --git a/cleanup_old_jellyfin.sh b/cleanup_old_jellyfin.sh new file mode 100755 index 0000000..af063e2 --- /dev/null +++ b/cleanup_old_jellyfin.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# ============================================================================== +# Opruimen van oude standalone jellyfin installatie op vmserver (10.2.0.4) +# ============================================================================== +set -euo pipefail + +CLUSTER_HOST="10.2.0.4" +SSH_USER="bart" + +COLOR_GREEN="\033[0;32m" +COLOR_YELLOW="\033[1;33m" +COLOR_RED="\033[0;31m" +COLOR_BLUE="\033[0;34m" +COLOR_RESET="\033[0m" + +log_info() { echo -e "${COLOR_GREEN}[INFO]${COLOR_RESET} $1"; } +log_step() { echo -e "\n${COLOR_BLUE}==>${COLOR_RESET} ${COLOR_GREEN}$1${COLOR_RESET}"; } +log_warn() { echo -e "${COLOR_YELLOW}[WARN]${COLOR_RESET} $1"; } + +echo "========================================================================" +echo " OPRUIMEN OUDE STANDALONE JELLYFIN OP ${CLUSTER_HOST}" +echo "========================================================================" +echo "Dit stopt en verwijdert de host-service configuratie en optioneel de" +echo "oude data-directories op ${CLUSTER_HOST}:" +echo " - jellyfin.service (stoppen en uitschakelen)" +echo " - /etc/systemd/system/jellyfin.service.d" +echo " - /var/cache/jellyfin" +echo "LET OP: Voer dit script pas uit nadat de Kubernetes pod succesvol draait" +echo "en geverifieerd is!" +echo "========================================================================" +echo "" + +read -p "Weet je zeker dat je de oude standalone jellyfin host service wilt opruimen? (j/N): " -r CONFIRM +if [[ ! "$CONFIRM" =~ ^[jJyY]$ ]]; then + log_info "Opruimen geannuleerd." + exit 0 +fi + +log_step "Uitschakelen van jellyfin.service en opruimen van host service bestanden..." +ssh -t "${SSH_USER}@${CLUSTER_HOST}" " + echo '--> Stoppen en masken van jellyfin.service...' + sudo systemctl stop jellyfin 2>/dev/null || true + sudo systemctl disable jellyfin 2>/dev/null || true + sudo systemctl mask jellyfin 2>/dev/null || true + + echo '--> Opruimen van cache...' + sudo rm -rf /var/cache/jellyfin + + echo '--> Opslagruimte controleren...' + df -h / /datadisk /storage +" + +echo "" +log_info "De oude standalone jellyfin host service is uitgeschakeld en gemaskeerd!" +echo "De Jellyfin media server draait nu volledig in het Kubernetes cluster." diff --git a/k8s/jellyfin/deployment.yaml b/k8s/jellyfin/deployment.yaml new file mode 100644 index 0000000..f1f2132 --- /dev/null +++ b/k8s/jellyfin/deployment.yaml @@ -0,0 +1,133 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jellyfin + namespace: htpc + labels: + app.kubernetes.io/name: jellyfin + app.kubernetes.io/part-of: htpc +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: jellyfin + template: + metadata: + labels: + app: jellyfin + app.kubernetes.io/name: jellyfin + app.kubernetes.io/part-of: htpc + spec: + nodeSelector: + kubernetes.io/hostname: vmserver + initContainers: + - name: migrate-config + image: lscr.io/linuxserver/jellyfin:latest + imagePullPolicy: IfNotPresent + command: + - /bin/bash + - -c + - | + if [ ! -f /config/system.xml ] && [ -f /host-etc/system.xml ]; then + echo "Syncing Jellyfin configuration from /etc/jellyfin..." + cp -a /host-etc/* /config/ + sed -i 's|/var/lib/jellyfin/metadata|/config/metadata|g' /config/system.xml || true + sed -i 's|/var/cache/jellyfin|/config/cache|g' /config/system.xml || true + fi + if [ ! -f /config/data/jellyfin.db ] && [ -d /host-data/data ]; then + echo "Syncing Jellyfin data, metadata, plugins and root from /var/lib/jellyfin..." + mkdir -p /config/data /config/metadata /config/plugins /config/root + [ -d /host-data/data ] && cp -a /host-data/data/* /config/data/ || true + [ -d /host-data/metadata ] && cp -a /host-data/metadata/* /config/metadata/ || true + [ -d /host-data/plugins ] && cp -a /host-data/plugins/* /config/plugins/ || true + [ -d /host-data/root ] && cp -a /host-data/root/* /config/root/ || true + fi + chown -R 33:33 /config + volumeMounts: + - name: config + mountPath: /config + - name: host-etc + mountPath: /host-etc + readOnly: true + - name: host-data + mountPath: /host-data + readOnly: true + containers: + - name: jellyfin + image: lscr.io/linuxserver/jellyfin:latest + imagePullPolicy: IfNotPresent + securityContext: + privileged: true + env: + - name: PUID + value: "33" + - name: PGID + value: "33" + - name: TZ + value: "Europe/Amsterdam" + - name: DOTNET_gcServer + value: "0" + - name: DOTNET_GCHeapHardLimit + value: "0x160000000" + - name: DOTNET_GCLargeObjectHeapCompactionMode + value: "1" + ports: + - name: http + containerPort: 8096 + hostPort: 8096 + resources: + requests: + cpu: 200m + memory: 1Gi + limits: + cpu: 4000m + memory: 8Gi + volumeMounts: + - name: config + mountPath: /config + - name: datadisk + mountPath: /datadisk + - name: mnt-mp3 + mountPath: /mnt/mp3 + - name: dri + mountPath: /dev/dri + livenessProbe: + httpGet: + path: /jellyfin/health + port: 8096 + initialDelaySeconds: 60 + periodSeconds: 20 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /jellyfin/health + port: 8096 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + volumes: + - name: config + persistentVolumeClaim: + claimName: jellyfin-config + - name: datadisk + hostPath: + path: /datadisk + type: Directory + - name: mnt-mp3 + hostPath: + path: /mnt/mp3 + type: Directory + - name: dri + hostPath: + path: /dev/dri + type: Directory + - name: host-etc + hostPath: + path: /etc/jellyfin + type: DirectoryOrCreate + - name: host-data + hostPath: + path: /var/lib/jellyfin + type: DirectoryOrCreate diff --git a/k8s/jellyfin/kustomization.yaml b/k8s/jellyfin/kustomization.yaml new file mode 100644 index 0000000..48bc1a4 --- /dev/null +++ b/k8s/jellyfin/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - pvc.yaml + - service.yaml + - deployment.yaml diff --git a/k8s/jellyfin/pvc.yaml b/k8s/jellyfin/pvc.yaml new file mode 100644 index 0000000..1227931 --- /dev/null +++ b/k8s/jellyfin/pvc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-config + namespace: htpc + labels: + app.kubernetes.io/name: jellyfin + app.kubernetes.io/part-of: htpc +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 30Gi diff --git a/k8s/jellyfin/service.yaml b/k8s/jellyfin/service.yaml new file mode 100644 index 0000000..211ae48 --- /dev/null +++ b/k8s/jellyfin/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: jellyfin + namespace: htpc + labels: + app.kubernetes.io/name: jellyfin + app.kubernetes.io/part-of: htpc +spec: + type: ClusterIP + ports: + - name: http + port: 8096 + targetPort: 8096 + protocol: TCP + selector: + app: jellyfin diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index e9b9029..0c07799 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -12,5 +12,6 @@ resources: - slskd - jackett - flaresolverr + - jellyfin diff --git a/migrate_jellyfin_to_cluster.sh b/migrate_jellyfin_to_cluster.sh new file mode 100755 index 0000000..5308b0c --- /dev/null +++ b/migrate_jellyfin_to_cluster.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# ============================================================================== +# Jellyfin Migration Script: Host Service -> Kubernetes Cluster (htpc namespace) +# ============================================================================== +set -euo pipefail + +CLUSTER_HOST="10.2.0.4" +SSH_USER="bart" +HTPC_NS="htpc" +BACKUP_DIR="/storage/backups" +DATE_STR=$(date +%Y-%m-%d) +KUBECONFIG_PATH="${KUBECONFIG:-$HOME/.kube/config_vmserver}" + +COLOR_GREEN="\033[0;32m" +COLOR_YELLOW="\033[1;33m" +COLOR_RED="\033[0;31m" +COLOR_BLUE="\033[0;34m" +COLOR_RESET="\033[0m" + +log_info() { echo -e "${COLOR_GREEN}[INFO]${COLOR_RESET} $1"; } +log_step() { echo -e "\n${COLOR_BLUE}==>${COLOR_RESET} ${COLOR_GREEN}$1${COLOR_RESET}"; } +log_warn() { echo -e "${COLOR_YELLOW}[WARN]${COLOR_RESET} $1"; } +log_err() { echo -e "${COLOR_RED}[ERROR]${COLOR_RESET} $1"; } + +echo "========================================================================" +echo " JELLYFIN MIGRATIE NAAR KUBERNETES CLUSTER (NAMESPACE: ${HTPC_NS})" +echo "========================================================================" +echo "Bron: jellyfin.service op ${CLUSTER_HOST}" +echo "Config: /etc/jellyfin & /var/lib/jellyfin" +echo "Doel: K8s Pod in namespace '${HTPC_NS}' op ${CLUSTER_HOST}" +echo "Poort: 8096 (ClusterIP & HostPort 8096)" +echo "Transcode: AMD VA-API (/dev/dri/renderD128)" +echo "========================================================================" +echo "" + +read -p "Weet je zeker dat je de Jellyfin migratie naar Kubernetes wilt starten? (j/N): " -r CONFIRM +if [[ ! "$CONFIRM" =~ ^[jJyY]$ ]]; then + log_info "Migratie geannuleerd." + exit 0 +fi + +# ------------------------------------------------------------------------------ +# STAP 1: PRE-FLIGHT CHECKS +# ------------------------------------------------------------------------------ +log_step "1. Pre-flight checks uitvoeren..." + +log_info "Verbinding testen naar host (${CLUSTER_HOST})..." +ssh -o BatchMode=yes -o ConnectTimeout=5 "${SSH_USER}@${CLUSTER_HOST}" "true" || { + log_err "Kan niet verbinden met host (${CLUSTER_HOST}) via SSH." + exit 1 +} + +log_info "Controleren van KUBECONFIG verbinding..." +KUBECONFIG="${KUBECONFIG_PATH}" kubectl get nodes | grep -q "vmserver" || { + log_err "Kan vmserver node niet bereiken via kubectl (KUBECONFIG=${KUBECONFIG_PATH})!" + exit 1 +} + +# ------------------------------------------------------------------------------ +# STAP 2: RESERVEKOPIE MAKEN VAN JELLYFIN CONFIG & DATA +# ------------------------------------------------------------------------------ +log_step "2. Reservekopie maken van /etc/jellyfin en /var/lib/jellyfin/data op ${CLUSTER_HOST}..." +ssh -t "${SSH_USER}@${CLUSTER_HOST}" " + sudo mkdir -p '${BACKUP_DIR}' + if [ -d '/etc/jellyfin' ] || [ -d '/var/lib/jellyfin/data' ]; then + echo 'Inpakken van Jellyfin configuratie en database naar ${BACKUP_DIR}/jellyfin_backup_${DATE_STR}.tar.gz...' + sudo tar -czf '${BACKUP_DIR}/jellyfin_backup_${DATE_STR}.tar.gz' \ + -C / etc/jellyfin \ + -C /var/lib/jellyfin data + echo 'Backup gereed:' + ls -lh '${BACKUP_DIR}/jellyfin_backup_${DATE_STR}.tar.gz' + else + echo 'Geen bestaande Jellyfin data gevonden om te back-uppen.' + fi +" + +# ------------------------------------------------------------------------------ +# STAP 3: STANDALONE JELLYFIN SERVICE STOPPEN OP DE HOST +# ------------------------------------------------------------------------------ +log_step "3. Standalone jellyfin.service stoppen op ${CLUSTER_HOST}..." +ssh -t "${SSH_USER}@${CLUSTER_HOST}" " + echo '--> Stoppen en uitschakelen van jellyfin.service...' + sudo systemctl stop jellyfin + sudo systemctl disable jellyfin +" + +# ------------------------------------------------------------------------------ +# STAP 4: KUBERNETES MANIFESTS TOEPASSEN +# ------------------------------------------------------------------------------ +log_step "4. Kubernetes manifests toepassen..." +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KUBECONFIG="${KUBECONFIG_PATH}" kubectl apply -k "${SCRIPT_DIR}/k8s/" + +log_info "Wachten op succesvolle uitrol van Jellyfin pod (eerste start kan enkele minuten duren voor datamigratie)..." +KUBECONFIG="${KUBECONFIG_PATH}" kubectl rollout status deployment/jellyfin -n "${HTPC_NS}" --timeout=300s + +# ------------------------------------------------------------------------------ +# STAP 5: VERIFICATIE +# ------------------------------------------------------------------------------ +log_step "5. Verificatie van endpoints..." +sleep 5 + +log_info "Health endpoint testen via host (poort 8096)..." +ssh -o BatchMode=yes "${SSH_USER}@${CLUSTER_HOST}" "curl -I -s http://127.0.0.1:8096/jellyfin/health" | head -n 1 + +log_info "Cluster IP service testen..." +CLUSTER_IP=$(KUBECONFIG="${KUBECONFIG_PATH}" kubectl get svc jellyfin -n "${HTPC_NS}" -o jsonpath='{.spec.clusterIP}') +ssh -o BatchMode=yes "${SSH_USER}@${CLUSTER_HOST}" "curl -I -s http://${CLUSTER_IP}:8096/jellyfin/health" | head -n 1 + +log_info "Controleer of render apparaten (/dev/dri) toegankelijk zijn in container..." +POD_NAME=$(KUBECONFIG="${KUBECONFIG_PATH}" kubectl get pod -n "${HTPC_NS}" -l app=jellyfin -o jsonpath='{.items[0].metadata.name}') +KUBECONFIG="${KUBECONFIG_PATH}" kubectl exec -n "${HTPC_NS}" "${POD_NAME}" -- ls -la /dev/dri || true + +echo "" +echo "========================================================================" +log_info "JELLYFIN MIGRATIE SUCCESVOL AFGEROND!" +echo "De Jellyfin media server draait nu in Kubernetes." +echo "Apache proxy: https://thuus.bnhmeijer.nl/jellyfin" +echo "========================================================================" diff --git a/rollback_jellyfin_migration.sh b/rollback_jellyfin_migration.sh new file mode 100755 index 0000000..4aeed8d --- /dev/null +++ b/rollback_jellyfin_migration.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# ============================================================================== +# Jellyfin Rollback Script: Kubernetes -> Host Standalone jellyfin.service +# ============================================================================== +set -euo pipefail + +CLUSTER_HOST="10.2.0.4" +SSH_USER="bart" +HTPC_NS="htpc" +KUBECONFIG_PATH="${KUBECONFIG:-$HOME/.kube/config_vmserver}" + +COLOR_GREEN="\033[0;32m" +COLOR_YELLOW="\033[1;33m" +COLOR_RED="\033[0;31m" +COLOR_BLUE="\033[0;34m" +COLOR_RESET="\033[0m" + +log_info() { echo -e "${COLOR_GREEN}[INFO]${COLOR_RESET} $1"; } +log_step() { echo -e "\n${COLOR_BLUE}==>${COLOR_RESET} ${COLOR_GREEN}$1${COLOR_RESET}"; } +log_warn() { echo -e "${COLOR_YELLOW}[WARN]${COLOR_RESET} $1"; } + +echo "========================================================================" +echo " JELLYFIN ROLLBACK NAAR STANDALONE SERVICE OP $CLUSTER_HOST" +echo "========================================================================" +read -p "Weet je zeker dat je wilt terugrollen naar standalone jellyfin? (j/N): " -r CONFIRM +if [[ ! "$CONFIRM" =~ ^[jJyY]$ ]]; then + log_info "Rollback geannuleerd." + exit 0 +fi + +log_step "1. Kubernetes Jellyfin deployment schalen naar 0..." +KUBECONFIG="${KUBECONFIG_PATH}" kubectl scale deployment/jellyfin -n "${HTPC_NS}" --replicas=0 || true + +log_step "2. Standalone jellyfin.service inschakelen en starten op $CLUSTER_HOST..." +ssh -t "${SSH_USER}@${CLUSTER_HOST}" "sudo systemctl enable --now jellyfin" + +log_step "3. Status controleren..." +sleep 5 +ssh -o BatchMode=yes "${SSH_USER}@${CLUSTER_HOST}" "systemctl status jellyfin --no-pager" | head -n 12 +ssh -o BatchMode=yes "${SSH_USER}@${CLUSTER_HOST}" "curl -I -s http://127.0.0.1:8096/jellyfin/health" | head -n 1 + +echo "" +log_info "Rollback voltooid. Standalone jellyfin.service is weer actief op de host."