summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-06 09:07:32 +0200
committerPaul Buetow <paul@buetow.org>2026-02-06 09:07:32 +0200
commitb1ed702825176ba93ad1ae754432cdb76765b9ff (patch)
tree438eedec751c14c186bc5e7b5d45424597fc2a94
parent4f4a67adbbde524b8c821f253bf1780ec5a2b054 (diff)
Restore jellyfin helm-chart and ArgoCD application
Co-authored-by: Cursor <cursoragent@cursor.com>
-rw-r--r--f3s/argocd-apps/services/jellyfin.yaml28
-rw-r--r--f3s/jellyfin/Justfile38
-rw-r--r--f3s/jellyfin/README.md86
-rw-r--r--f3s/jellyfin/SUMMARY.md135
-rw-r--r--f3s/jellyfin/helm-chart/Chart.yaml5
-rw-r--r--f3s/jellyfin/helm-chart/templates/.gitignore2
-rw-r--r--f3s/jellyfin/helm-chart/templates/configmap.yaml46
-rw-r--r--f3s/jellyfin/helm-chart/templates/deployment.yaml68
-rw-r--r--f3s/jellyfin/helm-chart/templates/ingress.yaml3
-rw-r--r--f3s/jellyfin/helm-chart/templates/persistent-volume.yaml86
-rw-r--r--f3s/jellyfin/values.yaml52
11 files changed, 549 insertions, 0 deletions
diff --git a/f3s/argocd-apps/services/jellyfin.yaml b/f3s/argocd-apps/services/jellyfin.yaml
new file mode 100644
index 0000000..f24974e
--- /dev/null
+++ b/f3s/argocd-apps/services/jellyfin.yaml
@@ -0,0 +1,28 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: jellyfin
+ namespace: cicd
+ finalizers:
+ - resources-finalizer.argocd.argoproj.io
+spec:
+ project: default
+ source:
+ repoURL: http://git-server.cicd.svc.cluster.local/conf.git
+ targetRevision: master
+ path: f3s/jellyfin/helm-chart
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: services
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=false
+ retry:
+ limit: 3
+ backoff:
+ duration: 5s
+ factor: 2
+ maxDuration: 1m
diff --git a/f3s/jellyfin/Justfile b/f3s/jellyfin/Justfile
new file mode 100644
index 0000000..46848de
--- /dev/null
+++ b/f3s/jellyfin/Justfile
@@ -0,0 +1,38 @@
+NAMESPACE := "services"
+APP_NAME := "jellyfin"
+
+status:
+ @echo "=== Pods ==="
+ @kubectl get pods -n {{NAMESPACE}} | grep jellyfin
+ @echo ""
+ @echo "=== Services ==="
+ @kubectl get svc -n {{NAMESPACE}} | grep jellyfin
+ @echo ""
+ @echo "=== Ingress ==="
+ @kubectl get ingress -n {{NAMESPACE}} jellyfin-ingress
+ @echo ""
+ @echo "=== PVCs ==="
+ @kubectl get pvc -n {{NAMESPACE}} | grep jellyfin
+ @echo ""
+ @echo "=== ArgoCD Status ==="
+ @kubectl get application {{APP_NAME}} -n cicd -o jsonpath='Sync: {.status.sync.status}, Health: {.status.health.status}' 2>/dev/null && echo ""
+
+logs lines="100":
+ kubectl logs -n {{NAMESPACE}} -l app=jellyfin-server --tail={{lines}} -f
+
+port-forward port="8096":
+ @echo "Forwarding jellyfin to localhost:{{port}}"
+ kubectl port-forward -n {{NAMESPACE}} svc/jellyfin-server {{port}}:8096
+
+sync:
+ @echo "Triggering ArgoCD sync..."
+ @kubectl annotate application {{APP_NAME}} -n cicd argocd.argoproj.io/refresh=normal --overwrite
+ @sleep 2
+ @kubectl get application {{APP_NAME}} -n cicd -o jsonpath='Sync: {.status.sync.status}, Health: {.status.health.status}' && echo ""
+
+argocd-status:
+ argocd app get {{APP_NAME}} --core
+
+restart:
+ @echo "Restarting jellyfin server..."
+ kubectl rollout restart -n {{NAMESPACE}} deployment/jellyfin-server
diff --git a/f3s/jellyfin/README.md b/f3s/jellyfin/README.md
new file mode 100644
index 0000000..ba08965
--- /dev/null
+++ b/f3s/jellyfin/README.md
@@ -0,0 +1,86 @@
+# Jellyfin Kubernetes Deployment
+
+This directory contains the Kubernetes configuration for deploying [Jellyfin](https://jellyfin.org/) - a free software media system that puts you in control of your media and data.
+
+## Architecture
+
+Jellyfin is a single-component deployment consisting of:
+- **Server**: Main media server with web interface and API
+
+## Prerequisites
+
+1. **Create storage directory on the NFS server**:
+ ```bash
+ for host in f0 f1 f2; do
+ ssh paul@$host "doas mkdir -p /data/nfs/k3svolumes/jellyfin"
+ ssh paul@$host "doas chown -R 911:911 /data/nfs/k3svolumes/jellyfin/"
+ done
+ ```
+
+## Deployment
+
+1. **Install the custom resources** (PVs, PVCs, ingress):
+ ```bash
+ just install-resources
+ ```
+
+2. **Install Jellyfin using Helm** (or ArgoCD):
+ ```bash
+ just sync
+ ```
+
+3. **Check deployment status**:
+ ```bash
+ just status
+ ```
+
+ Wait for all pods to be in `Running` state (may take a few minutes for image pulls).
+
+## Access
+
+Once deployed, Jellyfin will be available at: **https://jellyfin.f3s.buetow.org**
+
+Default setup instructions:
+1. Navigate to the URL above
+2. Complete the setup wizard on first access
+3. Configure libraries and preferences
+
+## Storage
+
+Persistent storage is configured with:
+- **Data**: Main configuration and metadata at `/data/nfs/k3svolumes/jellyfin`
+- **Media**: Mount your media directories from other NFS sources as needed
+
+## Maintenance
+
+### Restart Jellyfin
+```bash
+just restart
+```
+
+### View logs
+```bash
+just logs
+```
+
+### Port forward for local access
+```bash
+just port-forward
+```
+
+### Uninstall (keeps data)
+```bash
+kubectl delete application jellyfin -n cicd
+```
+
+## Troubleshooting
+
+### Check pod logs
+```bash
+kubectl logs -n services -l app=jellyfin-server --tail=100
+```
+
+### Verify persistent volumes
+```bash
+kubectl get pv,pvc -n services | grep jellyfin
+```
diff --git a/f3s/jellyfin/SUMMARY.md b/f3s/jellyfin/SUMMARY.md
new file mode 100644
index 0000000..a3ac82a
--- /dev/null
+++ b/f3s/jellyfin/SUMMARY.md
@@ -0,0 +1,135 @@
+# Jellyfin Deployment Summary
+
+## Objective
+Deploy Jellyfin 10.11.6 (latest stable) with proper reverse proxy configuration through relayd and Traefik, accessible at `https://jellyfin.f3s.buetow.org` and alternate ports 8096/8920 for Android app compatibility.
+
+## Configuration Implemented
+
+### 1. Kubernetes Resources
+- **Deployment**: Jellyfin server using `jellyfin/jellyfin:latest` image
+- **Service**: NodePort service exposing ports 30096 and 30920 for direct internal access
+- **PersistentVolumes/Claims**: Three volumes for config, libraries, and data
+- **Pod Resources**: 100m CPU request, 256Mi RAM request; 2000m CPU limit, 2Gi RAM limit
+
+### 2. Reverse Proxy Configuration (relayd)
+
+#### Frontend Setup
+- **TLS Termination**: Relayd listens on ports 443 (IPv4/IPv6) with Let's Encrypt certificates
+- **Header Forwarding**:
+ - `X-Forwarded-For: $REMOTE_ADDR` (client IP)
+ - `X-Forwarded-Proto: https` (protocol indication)
+- **Multiple Ports**: Added separate relay rules for ports 8096 and 8920 to support Android app discovery attempts
+
+#### Routing Rules
+- **Port 443**: Routes to Jellyfin NodePort 30096 via `f3s_jellyfin` backend table
+- **Ports 8096/8920**: Dual IPv4/IPv6 relays also forward to NodePort 30096
+- **Host Routing**: Explicit match for `jellyfin.f3s.buetow.org` hostname
+
+### 3. Jellyfin Network Configuration
+- **RequireHttps**: false (TLS handled by relayd)
+- **EnableHttps**: false (no self-signed certs)
+- **PublicPort**: 443 (external port users connect to)
+- **KnownProxies**:
+ - 10.0.0.0/8 (Kubernetes cluster CIDR)
+ - 192.168.0.0/16 (relayd/frontend subnet)
+- **EnablePublishedServerUriByRequest**: false
+
+### 4. Certificate Chain
+- **Full Chain**: Relayd presents complete certificate chain (leaf + R12 intermediate)
+- **Validation**: Confirmed with `openssl s_client` showing 2 certificates
+- **Auto-renewal**: Let's Encrypt certificates on relayd
+
+## Issues Encountered & Solutions
+
+### Issue 1: Database Migration Failures
+- **Problem**: Upgrading from 10.8.13 → 10.11.6 directly caused database corruption
+- **Solution**: Requires upgrade path 10.8.13 → 10.10.7 → 10.11.6
+- **Status**: Settled on `jellyfin:latest` (10.11.6) with clean database
+
+### Issue 2: ConfigMap Read-Only Mount
+- **Problem**: Network.xml mounted as read-only ConfigMap; newer Jellyfin versions need to write during migration
+- **Solution**: Removed ConfigMap mount, let Jellyfin manage network.xml from PVC
+- **Result**: Cleaner configuration, Jellyfin can self-manage settings
+
+### Issue 3: Android App "Unsupported version or product" Error
+- **Root Cause**:
+ - Missing full certificate chain from relayd → Android app SSL validation failure
+ - App attempting alternate ports (8096, 8920) that weren't exposed
+- **Solution**:
+ - Added relayd relays for ports 8096 and 8920
+ - Ensured full cert chain is presented
+ - App should now connect to any of the three ports
+
+### Issue 4: NFS Storage Read-Only (CURRENT BLOCKER)
+- **Problem**: `/data/nfs/k3svolumes/jellyfin/*` directories mounted read-only
+- **Error**: `chown` and pod writes fail with "Read-only file system"
+- **Status**: PVCs remain Pending; pods cannot start
+- **Resolution Required**: NFS mount needs to be remounted as read-write on f0/f1/f2 hosts
+
+## Current Deployment Status
+
+✅ **Complete**
+- Kubernetes manifests fully configured
+- ArgoCD Application re-enabled with proper git URL
+- Relayd configuration updated and deployed
+- Certificate chain verified
+- All networking rules in place
+
+❌ **Blocked**
+- PersistentVolumes cannot bind to PVCs (read-only NFS)
+- Jellyfin pod remains in Pending state
+- Cannot proceed with testing until NFS is writable
+
+## Next Steps
+
+1. **Fix NFS Mount** (blocking issue)
+ ```bash
+ # On f0, f1, f2 - remount /data with write permissions
+ doas mount -uw /data
+ # Or check NFS export configuration
+ ```
+
+2. **Deploy & Test**
+ - Once NFS is writable, pods will automatically start via ArgoCD
+ - Test connectivity: `curl https://jellyfin.f3s.buetow.org/System/Info/Public`
+ - Test Android app with manual URL entry
+
+3. **Configure Jellyfin** (post-deployment)
+ - Run setup wizard
+ - Add media libraries
+ - Configure transcoding if needed
+ - Verify Android app can connect
+
+## Key Files
+
+- **Deployment**: `jellyfin/helm-chart/templates/deployment.yaml`
+- **Persistent Storage**: `jellyfin/helm-chart/templates/persistent-volume.yaml`
+- **Relayd Config**: `/home/paul/git/conf/frontends/etc/relayd.conf.tpl` (lines ~15-130)
+- **ArgoCD App**: Created via kubectl in services namespace
+
+## Testing Commands
+
+```bash
+# Check pod status
+kubectl get pods -n services -l app=jellyfin-server
+
+# View logs
+kubectl logs -n services -l app=jellyfin-server
+
+# Test API endpoint
+curl https://jellyfin.f3s.buetow.org/System/Info/Public
+
+# Verify certificate chain
+echo | openssl s_client -servername jellyfin.f3s.buetow.org -connect jellyfin.f3s.buetow.org:443 | grep "BEGIN CERTIFICATE" | wc -l
+# Should output: 2
+
+# Check PVC binding
+kubectl get pvc -n services | grep jellyfin
+```
+
+## Notes
+
+- Latest version (10.11.6) requires database >= 10.9.11
+- Android app compatibility improved in 10.10.7+
+- Relayd provides full TLS termination, reducing complexity vs. Traefik double-proxy
+- NodePort approach bypasses Traefik, avoiding header forwarding issues
diff --git a/f3s/jellyfin/helm-chart/Chart.yaml b/f3s/jellyfin/helm-chart/Chart.yaml
new file mode 100644
index 0000000..07982e5
--- /dev/null
+++ b/f3s/jellyfin/helm-chart/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v2
+name: jellyfin-resources
+description: Custom resources for Jellyfin deployment (PVs, PVCs, Ingress)
+version: 0.1.0
+appVersion: "10.11.6"
diff --git a/f3s/jellyfin/helm-chart/templates/.gitignore b/f3s/jellyfin/helm-chart/templates/.gitignore
new file mode 100644
index 0000000..8eeb559
--- /dev/null
+++ b/f3s/jellyfin/helm-chart/templates/.gitignore
@@ -0,0 +1,2 @@
+# Secrets should not be committed
+*-secret.yaml
diff --git a/f3s/jellyfin/helm-chart/templates/configmap.yaml b/f3s/jellyfin/helm-chart/templates/configmap.yaml
new file mode 100644
index 0000000..5ce6fe4
--- /dev/null
+++ b/f3s/jellyfin/helm-chart/templates/configmap.yaml
@@ -0,0 +1,46 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: jellyfin-network-config
+ namespace: services
+data:
+ network.xml: |
+ <?xml version="1.0" encoding="utf-8"?>
+ <NetworkConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <RequireHttps>false</RequireHttps>
+ <CertificatePath />
+ <CertificatePassword />
+ <BaseUrl />
+ <PublicHttpsPort>443</PublicHttpsPort>
+ <HttpServerPortNumber>8096</HttpServerPortNumber>
+ <HttpsPortNumber>8920</HttpsPortNumber>
+ <EnableHttps>false</EnableHttps>
+ <PublicPort>443</PublicPort>
+ <UPnPCreateHttpPortMap>false</UPnPCreateHttpPortMap>
+ <UDPPortRange />
+ <EnableIPV6>false</EnableIPV6>
+ <EnableIPV4>true</EnableIPV4>
+ <EnableSSDPTracing>false</EnableSSDPTracing>
+ <SSDPTracingFilter />
+ <UDPSendCount>2</UDPSendCount>
+ <UDPSendDelay>100</UDPSendDelay>
+ <IgnoreVirtualInterfaces>true</IgnoreVirtualInterfaces>
+ <VirtualInterfaceNames>vEthernet*</VirtualInterfaceNames>
+ <GatewayMonitorPeriod>60</GatewayMonitorPeriod>
+ <TrustAllIP6Interfaces>false</TrustAllIP6Interfaces>
+ <HDHomerunPortRange />
+ <PublishedServerUriBySubnet />
+ <AutoDiscoveryTracing>false</AutoDiscoveryTracing>
+ <AutoDiscovery>true</AutoDiscovery>
+ <RemoteIPFilter />
+ <IsRemoteIPFilterBlacklist>false</IsRemoteIPFilterBlacklist>
+ <EnableUPnP>false</EnableUPnP>
+ <EnableRemoteAccess>true</EnableRemoteAccess>
+ <LocalNetworkSubnets />
+ <LocalNetworkAddresses />
+ <KnownProxies>
+ <ProxyAddress>10.0.0.0/8</ProxyAddress>
+ <ProxyAddress>192.168.0.0/16</ProxyAddress>
+ </KnownProxies>
+ <EnablePublishedServerUriByRequest>false</EnablePublishedServerUriByRequest>
+ </NetworkConfiguration>
diff --git a/f3s/jellyfin/helm-chart/templates/deployment.yaml b/f3s/jellyfin/helm-chart/templates/deployment.yaml
new file mode 100644
index 0000000..0289dfe
--- /dev/null
+++ b/f3s/jellyfin/helm-chart/templates/deployment.yaml
@@ -0,0 +1,68 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: jellyfin-server
+ namespace: services
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: jellyfin-server
+ template:
+ metadata:
+ labels:
+ app: jellyfin-server
+ spec:
+ containers:
+ - name: jellyfin
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ ports:
+ - containerPort: 8096
+ name: http
+ resources:
+ requests:
+ cpu: 500m
+ memory: 512Mi
+ limits:
+ cpu: 4000m
+ memory: 4Gi
+ volumeMounts:
+ - name: jellyfin-config
+ mountPath: /config
+ - name: jellyfin-libraries
+ mountPath: /media/libraries
+ - name: jellyfin-data
+ mountPath: /data
+ volumes:
+ - name: jellyfin-config
+ persistentVolumeClaim:
+ claimName: jellyfin-config-pvc
+ - name: jellyfin-libraries
+ persistentVolumeClaim:
+ claimName: jellyfin-libraries-pvc
+ - name: jellyfin-data
+ persistentVolumeClaim:
+ claimName: jellyfin-data-pvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: jellyfin-server
+ name: jellyfin-server
+ namespace: services
+spec:
+ type: NodePort
+ ports:
+ - name: http
+ port: 8096
+ protocol: TCP
+ targetPort: 8096
+ nodePort: 30096
+ - name: https
+ port: 8920
+ protocol: TCP
+ targetPort: 8096
+ nodePort: 30920
+ selector:
+ app: jellyfin-server
diff --git a/f3s/jellyfin/helm-chart/templates/ingress.yaml b/f3s/jellyfin/helm-chart/templates/ingress.yaml
new file mode 100644
index 0000000..ed01eca
--- /dev/null
+++ b/f3s/jellyfin/helm-chart/templates/ingress.yaml
@@ -0,0 +1,3 @@
+# Jellyfin is exposed via NodePorts 30096 and 30920
+# relayd on frontend proxies directly to these ports, bypassing Traefik
+# This avoids double reverse-proxy complexity and certificate chain issues
diff --git a/f3s/jellyfin/helm-chart/templates/persistent-volume.yaml b/f3s/jellyfin/helm-chart/templates/persistent-volume.yaml
new file mode 100644
index 0000000..f32783e
--- /dev/null
+++ b/f3s/jellyfin/helm-chart/templates/persistent-volume.yaml
@@ -0,0 +1,86 @@
+# Jellyfin PersistentVolume - Configuration and metadata storage
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: jellyfin-config-pv
+spec:
+ capacity:
+ storage: 50Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ hostPath:
+ path: /data/nfs/k3svolumes/jellyfin/config
+ type: Directory
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: jellyfin-config-pvc
+ namespace: services
+spec:
+ storageClassName: ""
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 50Gi
+---
+# Jellyfin PersistentVolume - Libraries storage
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: jellyfin-libraries-pv
+spec:
+ capacity:
+ storage: 500Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ hostPath:
+ path: /data/nfs/k3svolumes/jellyfin/libraries
+ type: Directory
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: jellyfin-libraries-pvc
+ namespace: services
+spec:
+ storageClassName: ""
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 500Gi
+---
+# Jellyfin PersistentVolume - Data storage
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: jellyfin-data-pv
+spec:
+ capacity:
+ storage: 100Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ hostPath:
+ path: /data/nfs/k3svolumes/jellyfin/data
+ type: Directory
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: jellyfin-data-pvc
+ namespace: services
+spec:
+ storageClassName: ""
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 100Gi
diff --git a/f3s/jellyfin/values.yaml b/f3s/jellyfin/values.yaml
new file mode 100644
index 0000000..abcfc26
--- /dev/null
+++ b/f3s/jellyfin/values.yaml
@@ -0,0 +1,52 @@
+# Jellyfin Helm Chart Configuration
+# Deploy to services namespace with persistent storage
+
+# Image configuration
+image:
+ repository: jellyfin/jellyfin
+ tag: "10.11.6"
+ pullPolicy: IfNotPresent
+
+# Persistence
+persistence:
+ config:
+ enabled: true
+ type: pvc
+ existingClaim: jellyfin-pvc
+ mountPath: /config
+ size: 50Gi
+
+# Service configuration
+service:
+ type: ClusterIP
+ port: 8096
+
+# Ingress configuration
+ingress:
+ enabled: true
+ ingressClassName: traefik
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+ hosts:
+ - host: jellyfin.f3s.buetow.org
+ paths:
+ - path: /
+ pathType: Prefix
+
+# Resources
+resources:
+ requests:
+ cpu: 500m
+ memory: 512Mi
+ limits:
+ cpu: 4000m
+ memory: 4Gi
+
+# Node selector (if needed)
+nodeSelector: {}
+
+# Tolerations (if needed)
+tolerations: []
+
+# Affinity rules (if needed)
+affinity: {}