From a29fa5b8cc88c3f50554419d05dfa21254d4920e Mon Sep 17 00:00:00 2001 From: Ben Vin Date: Sun, 5 Jul 2026 22:32:24 +1000 Subject: [PATCH] Add jellyfin (HA fork) app under a new media project Deploys the jellyfin-ha fork (git.unkin.net/unkin/jellyfin-ha) to au-syd1 via ArgoCD, under a dedicated media AppProject/ApplicationSet rather than extending platform. - New media AppProject + media-apps ApplicationSet (watches apps/overlays/*/jellyfin); registered in the argocd kustomizations - apps/base/jellyfin: namespace, deployment (single replica to start), service, in-namespace Redis (transcode session store), gateway + httproute at jellyfin.k8s.syd1.au.unkin.net - Storage: RWO config (cephrbd), RWX transcode scratch and RWX media library (cephfs) per the HA fork's pod-takeover requirement - au-syd1 overlay --- apps/base/jellyfin/deployment.yaml | 80 +++++++++++++++++++ apps/base/jellyfin/gateway.yaml | 37 +++++++++ apps/base/jellyfin/httproute.yaml | 49 ++++++++++++ apps/base/jellyfin/kustomization.yaml | 16 ++++ apps/base/jellyfin/namespace.yaml | 5 ++ apps/base/jellyfin/pvc-config.yaml | 15 ++++ apps/base/jellyfin/pvc-media.yaml | 17 ++++ apps/base/jellyfin/pvc-transcode.yaml | 17 ++++ apps/base/jellyfin/redis-deployment.yaml | 66 +++++++++++++++ apps/base/jellyfin/redis-pvc.yaml | 14 ++++ apps/base/jellyfin/redis-service.yaml | 17 ++++ apps/base/jellyfin/service.yaml | 17 ++++ .../au-syd1/jellyfin/kustomization.yaml | 6 ++ argocd/applicationsets/kustomization.yaml | 1 + argocd/applicationsets/media.yaml | 31 +++++++ argocd/projects/kustomization.yaml | 1 + argocd/projects/media.yaml | 19 +++++ 17 files changed, 408 insertions(+) create mode 100644 apps/base/jellyfin/deployment.yaml create mode 100644 apps/base/jellyfin/gateway.yaml create mode 100644 apps/base/jellyfin/httproute.yaml create mode 100644 apps/base/jellyfin/kustomization.yaml create mode 100644 apps/base/jellyfin/namespace.yaml create mode 100644 apps/base/jellyfin/pvc-config.yaml create mode 100644 apps/base/jellyfin/pvc-media.yaml create mode 100644 apps/base/jellyfin/pvc-transcode.yaml create mode 100644 apps/base/jellyfin/redis-deployment.yaml create mode 100644 apps/base/jellyfin/redis-pvc.yaml create mode 100644 apps/base/jellyfin/redis-service.yaml create mode 100644 apps/base/jellyfin/service.yaml create mode 100644 apps/overlays/au-syd1/jellyfin/kustomization.yaml create mode 100644 argocd/applicationsets/media.yaml create mode 100644 argocd/projects/media.yaml diff --git a/apps/base/jellyfin/deployment.yaml b/apps/base/jellyfin/deployment.yaml new file mode 100644 index 0000000..7aaed32 --- /dev/null +++ b/apps/base/jellyfin/deployment.yaml @@ -0,0 +1,80 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jellyfin + namespace: jellyfin +spec: + # Start single-replica. Scaling to >1 (true HA) is a follow-up once the Redis + # transcode store and RWX transcode scratch are validated end-to-end. + replicas: 1 + strategy: + # Config PVC is RWO; a Recreate rollout avoids two pods contending for it. + type: Recreate + selector: + matchLabels: + app: jellyfin + template: + metadata: + labels: + app: jellyfin + spec: + containers: + - name: jellyfin + image: git.unkin.net/unkin/jellyfin-ha:v0.1.0 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8096 + protocol: TCP + env: + # Distributed transcode session store (jellyfin-ha additions). + - name: Jellyfin__TranscodeStore__RedisConnectionString + value: "redis:6379,abortConnect=false" + - name: Jellyfin__TranscodeStore__LeaseDurationSeconds + value: "30" + 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 + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "4" + memory: 6Gi + volumeMounts: + - name: config + mountPath: /config + - name: cache + mountPath: /cache + - name: transcode + mountPath: /transcode + - name: media + mountPath: /media + readOnly: true + volumes: + - name: config + persistentVolumeClaim: + claimName: jellyfin-config + - name: cache + emptyDir: {} + - name: transcode + persistentVolumeClaim: + claimName: jellyfin-transcode + - name: media + persistentVolumeClaim: + claimName: jellyfin-media diff --git a/apps/base/jellyfin/gateway.yaml b/apps/base/jellyfin/gateway.yaml new file mode 100644 index 0000000..a491519 --- /dev/null +++ b/apps/base/jellyfin/gateway.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + labels: + traefik.io/instance: internal + annotations: + cert-manager.io/cluster-issuer: vault-issuer + cert-manager.io/common-name: jellyfin.k8s.syd1.au.unkin.net + cert-manager.io/private-key-size: "4096" + external-dns.alpha.kubernetes.io/hostname: jellyfin.k8s.syd1.au.unkin.net + external-dns.alpha.kubernetes.io/target: 198.18.200.4 + name: jellyfin + namespace: jellyfin +spec: + gatewayClassName: traefik-internal + listeners: + - allowedRoutes: + namespaces: + from: Same + hostname: jellyfin.k8s.syd1.au.unkin.net + name: http + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: Same + hostname: jellyfin.k8s.syd1.au.unkin.net + name: https + port: 443 + protocol: HTTPS + tls: + certificateRefs: + - group: "" + kind: Secret + name: jellyfin-tls + mode: Terminate diff --git a/apps/base/jellyfin/httproute.yaml b/apps/base/jellyfin/httproute.yaml new file mode 100644 index 0000000..1778dcd --- /dev/null +++ b/apps/base/jellyfin/httproute.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-redirect + namespace: jellyfin +spec: + hostnames: + - jellyfin.k8s.syd1.au.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: jellyfin + sectionName: http + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 + matches: + - path: + type: PathPrefix + value: / +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: jellyfin-route + namespace: jellyfin +spec: + hostnames: + - jellyfin.k8s.syd1.au.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: jellyfin + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: jellyfin + port: 8096 + weight: 1 + matches: + - path: + type: PathPrefix + value: / diff --git a/apps/base/jellyfin/kustomization.yaml b/apps/base/jellyfin/kustomization.yaml new file mode 100644 index 0000000..b0ada69 --- /dev/null +++ b/apps/base/jellyfin/kustomization.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - pvc-config.yaml + - pvc-transcode.yaml + - pvc-media.yaml + - deployment.yaml + - service.yaml + - redis-deployment.yaml + - redis-pvc.yaml + - redis-service.yaml + - gateway.yaml + - httproute.yaml diff --git a/apps/base/jellyfin/namespace.yaml b/apps/base/jellyfin/namespace.yaml new file mode 100644 index 0000000..3122e11 --- /dev/null +++ b/apps/base/jellyfin/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: jellyfin diff --git a/apps/base/jellyfin/pvc-config.yaml b/apps/base/jellyfin/pvc-config.yaml new file mode 100644 index 0000000..3bef28c --- /dev/null +++ b/apps/base/jellyfin/pvc-config.yaml @@ -0,0 +1,15 @@ +--- +# Jellyfin config + SQLite library database. Single-writer, block storage. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-config + namespace: jellyfin +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi + storageClassName: cephrbd-fast-retain + volumeMode: Filesystem diff --git a/apps/base/jellyfin/pvc-media.yaml b/apps/base/jellyfin/pvc-media.yaml new file mode 100644 index 0000000..8b60434 --- /dev/null +++ b/apps/base/jellyfin/pvc-media.yaml @@ -0,0 +1,17 @@ +--- +# Media library, shared read-many across replicas. Retain — this holds the +# actual media and must survive PVC deletion. Empty on first deploy; populating +# it is out of scope for this app. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-media + namespace: jellyfin +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Ti + storageClassName: cephfs-raid6-retain + volumeMode: Filesystem diff --git a/apps/base/jellyfin/pvc-transcode.yaml b/apps/base/jellyfin/pvc-transcode.yaml new file mode 100644 index 0000000..b33c821 --- /dev/null +++ b/apps/base/jellyfin/pvc-transcode.yaml @@ -0,0 +1,17 @@ +--- +# Shared transcode scratch. ReadWriteMany is the hard requirement for the HA +# fork: a taking-over pod must read the in-flight HLS segments written by the +# pod it replaces. Scratch data, so delete reclaim policy. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-transcode + namespace: jellyfin +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 100Gi + storageClassName: cephfs-raid6-delete + volumeMode: Filesystem diff --git a/apps/base/jellyfin/redis-deployment.yaml b/apps/base/jellyfin/redis-deployment.yaml new file mode 100644 index 0000000..eeae15b --- /dev/null +++ b/apps/base/jellyfin/redis-deployment.yaml @@ -0,0 +1,66 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: jellyfin +spec: + replicas: 1 + selector: + matchLabels: + app: redis + strategy: + type: Recreate + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7-alpine + imagePullPolicy: IfNotPresent + command: + - redis-server + - --save + - "20" + - "1" + ports: + - containerPort: 6379 + name: redis + protocol: TCP + livenessProbe: + exec: + command: + - redis-cli + - ping + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - redis-cli + - ping + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + volumeMounts: + - mountPath: /data + name: data + restartPolicy: Always + volumes: + - name: data + persistentVolumeClaim: + claimName: jellyfin-redis-data diff --git a/apps/base/jellyfin/redis-pvc.yaml b/apps/base/jellyfin/redis-pvc.yaml new file mode 100644 index 0000000..36a4315 --- /dev/null +++ b/apps/base/jellyfin/redis-pvc.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-redis-data + namespace: jellyfin +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: cephrbd-fast-delete + volumeMode: Filesystem diff --git a/apps/base/jellyfin/redis-service.yaml b/apps/base/jellyfin/redis-service.yaml new file mode 100644 index 0000000..6e0e34f --- /dev/null +++ b/apps/base/jellyfin/redis-service.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: jellyfin +spec: + internalTrafficPolicy: Cluster + ports: + - name: redis + port: 6379 + protocol: TCP + targetPort: redis + selector: + app: redis + sessionAffinity: None + type: ClusterIP diff --git a/apps/base/jellyfin/service.yaml b/apps/base/jellyfin/service.yaml new file mode 100644 index 0000000..38b5c1d --- /dev/null +++ b/apps/base/jellyfin/service.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: jellyfin + namespace: jellyfin +spec: + internalTrafficPolicy: Cluster + ports: + - name: http + port: 8096 + protocol: TCP + targetPort: http + selector: + app: jellyfin + sessionAffinity: None + type: ClusterIP diff --git a/apps/overlays/au-syd1/jellyfin/kustomization.yaml b/apps/overlays/au-syd1/jellyfin/kustomization.yaml new file mode 100644 index 0000000..d8d5df6 --- /dev/null +++ b/apps/overlays/au-syd1/jellyfin/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/jellyfin diff --git a/argocd/applicationsets/kustomization.yaml b/argocd/applicationsets/kustomization.yaml index 87d07c2..66519e0 100644 --- a/argocd/applicationsets/kustomization.yaml +++ b/argocd/applicationsets/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - aitooling.yaml + - media.yaml - observability.yaml - platform.yaml - storage.yaml diff --git a/argocd/applicationsets/media.yaml b/argocd/applicationsets/media.yaml new file mode 100644 index 0000000..9fe99a5 --- /dev/null +++ b/argocd/applicationsets/media.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: media-apps + namespace: argocd +spec: + generators: + - git: + repoURL: https://git.unkin.net/unkin/argocd-apps + revision: HEAD + directories: + - path: apps/overlays/*/jellyfin + template: + metadata: + name: 'media-{{path[3]}}' + spec: + project: media + source: + repoURL: https://git.unkin.net/unkin/argocd-apps + targetRevision: HEAD + path: '{{path}}' + destination: + server: https://kubernetes.default.svc + namespace: '{{path[3]}}' + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - ServerSideApply=true diff --git a/argocd/projects/kustomization.yaml b/argocd/projects/kustomization.yaml index 87d07c2..66519e0 100644 --- a/argocd/projects/kustomization.yaml +++ b/argocd/projects/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - aitooling.yaml + - media.yaml - observability.yaml - platform.yaml - storage.yaml diff --git a/argocd/projects/media.yaml b/argocd/projects/media.yaml new file mode 100644 index 0000000..652c226 --- /dev/null +++ b/argocd/projects/media.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: media + namespace: argocd +spec: + description: Media services + sourceRepos: + - https://git.unkin.net/unkin/argocd-apps + destinations: + - namespace: 'jellyfin' + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: '' + kind: Namespace + namespaceResourceWhitelist: + - group: '*' + kind: '*'