diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-06 09:07:32 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-06 09:07:32 +0200 |
| commit | b1ed702825176ba93ad1ae754432cdb76765b9ff (patch) | |
| tree | 438eedec751c14c186bc5e7b5d45424597fc2a94 | |
| parent | 4f4a67adbbde524b8c821f253bf1780ec5a2b054 (diff) | |
Restore jellyfin helm-chart and ArgoCD application
Co-authored-by: Cursor <cursoragent@cursor.com>
| -rw-r--r-- | f3s/argocd-apps/services/jellyfin.yaml | 28 | ||||
| -rw-r--r-- | f3s/jellyfin/Justfile | 38 | ||||
| -rw-r--r-- | f3s/jellyfin/README.md | 86 | ||||
| -rw-r--r-- | f3s/jellyfin/SUMMARY.md | 135 | ||||
| -rw-r--r-- | f3s/jellyfin/helm-chart/Chart.yaml | 5 | ||||
| -rw-r--r-- | f3s/jellyfin/helm-chart/templates/.gitignore | 2 | ||||
| -rw-r--r-- | f3s/jellyfin/helm-chart/templates/configmap.yaml | 46 | ||||
| -rw-r--r-- | f3s/jellyfin/helm-chart/templates/deployment.yaml | 68 | ||||
| -rw-r--r-- | f3s/jellyfin/helm-chart/templates/ingress.yaml | 3 | ||||
| -rw-r--r-- | f3s/jellyfin/helm-chart/templates/persistent-volume.yaml | 86 | ||||
| -rw-r--r-- | f3s/jellyfin/values.yaml | 52 |
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: {} |
