Jellyfin is the household open-source media server. It is installed only by Argo CD Application jellyfin, which pins the official Helm chart jellyfin at 3.2.0 (image tag 10.11.8) into namespace jellyfin. The GitOps file shape matches Plex: public companion manifests, public values, a private overlay, and the upstream chart. Plex stays a LAN LoadBalancer; Jellyfin keeps a ClusterIP on port 8096 and is reached the way Immich is reached, through the existing Cloudflare Tunnel.
This page sits with the other application guides. Site hostnames and NAS coordinates stay in homelabs-private/clusters/<cluster>/.
GitOps#
Application jellyfin is multi-source. Public homelabs/jellyfin/manifests apply the namespace and the Longhorn config claim. Private clusters/<cluster>/manifests/jellyfin apply the NFS media volume. Helm merges $homelabs/jellyfin/values.yaml with $private/clusters/<cluster>/overlays/jellyfin/values.yaml onto chart jellyfin 3.2.0 from https://jellyfin.github.io/jellyfin-helm. Sync-wave is 1; CreateNamespace is on.
| Piece | Where | Role |
|---|---|---|
| Application CR | clusters/<cluster>/argocd/applications/jellyfin.yaml | Multi-source app |
| Public values | homelabs/jellyfin/values.yaml | ClusterIP :8096, claim names, Recreate, metrics off |
| Private overlay | clusters/<cluster>/overlays/jellyfin/values.yaml | Repeats ClusterIP :8096, does not convert the Service |
| Config PVC | homelabs/jellyfin/manifests/pvc-config.yaml | jellyfin-config-pvc-lh |
| Media PV/PVC | clusters/<cluster>/manifests/jellyfin/ | jellyfin-media-pvc on nfs-csi |
Public values set fullnameOverride: jellyfin, one replica, serviceAccount.automount: false, ingress and HTTPRoute off, and DLNA off. They leave metrics.enabled and metrics.serviceMonitor.enabled false: Jellyfin core has no Prometheus /metrics without a plugin, so a ServiceMonitor would scrape-fail. Cache is not a PVC (persistence.cache.enabled: false). The same file requests 250m CPU / 1Gi memory and limits 2 CPU / 4Gi.
AppProject homelab must allow the official chart repo and destination namespace jellyfin. Apply the live project before the Application is committed:
kubectl apply -f homelabs-private/argocd/projects/homelab.yamlDay-to-day changes go through Git and an Argo sync. Do not helm install Jellyfin from a workstation. Pin stays 3.2.0; treat a 3.3.x chart bump as a new change.
Public hostname#
The connector is the existing cloudflared Deployment in namespace cloudflare-tunnel. You do not add a second tunnel, and you do not edit homelabs/cloudflare/. In Zero Trust, add a Public Hostname on that tunnel:
| Field | Value |
|---|---|
| Subdomain | jellyfin (published as jellyfin.<your-domain>) |
| Type | HTTP |
| URL | http://jellyfin.jellyfin.svc.cluster.local:8096 |
| Cloudflare Access | On |
Put Access in front of this hostname before it answers. Use an email allow-list or an existing household identity group, not Everyone, and not an empty policy. Dex and Grafana stay Access-off because they are an identity issuer and its callback; Jellyfin is neither, so Access stays on after first-run as well. Clients that cannot complete an Access challenge (some official TV apps) are a later review, not a reason to turn Access off here.
Dashboard route changes apply without restarting cloudflared. The connector itself is documented under Cloudflare Tunnel.
First-run admin#
Anyone who can hit :8096 before an admin exists becomes the owner. Finish the native wizard on a path that is not public, then publish the hostname with Access already attached.
- Sync Application
jellyfinand confirm Servicejellyfinis ClusterIP with Ready endpoints and no EXTERNAL-IP. There is no Public Hostname yet. - Forward the Service and open the wizard on your workstation:
kubectl -n jellyfin port-forward svc/jellyfin 8096:8096- Create the first admin. Disable open registration in Jellyfin before you continue.
- Then create the Public Hostname above, with Access already on.
Do not run the wizard on a world-open origin, and do not create the hostname before Access exists.
There is no ExternalSecret, Vault path, Dex client, or in-app Google SSO in v1. Access at the Cloudflare edge is the identity gate; the admin password lives in config on the Longhorn claim after the wizard.
Storage#
Config state is claim jellyfin-config-pvc-lh: longhorn-ssd, ReadWriteOnce, 5Gi, GitOps from homelabs/jellyfin/manifests/pvc-config.yaml and Helm persistence.config.existingClaim. The Deployment strategy is Recreate so that single RWO volume can remount after a replace. RollingUpdate plus RWO deadlocks.
Media is a new claim jellyfin-media-pvc in namespace jellyfin (nfs-csi, ReadWriteMany, 1Ti). The private PersistentVolume points at the same NAS export Plex already uses. You do not remount plex-media-pvc, and you do not run Jellyfin in namespace plex. PVCs are namespaced; a second claim on the same export shares files without colocating the apps. Navidrome adds a third claim on that parent export, mounts it read-only, and indexes the music subdirectory. NAS server and share belong only in clusters/<cluster>/manifests/jellyfin/, not in public git and not on this page.
Keep library metadata on the config PVC. Leave media-folder NFO and sidecars off so two scanners do not write into the shared tree.
Confirm both claims after sync:
kubectl -n jellyfin get pvc
kubectl -n plex get pvc plex-media-pvcjellyfin-config-pvc-lh should be Bound on longhorn-ssd, jellyfin-media-pvc Bound on nfs-csi, and the Plex media claim still Bound. No container in jellyfin should mount plex-media-pvc.
Deploy#
Commit order is homelabs (feature branch + PR so jellyfin/ exists on main) → homelabs-private on main (AppProject apply first, then overlay, media volume, Application) → this docs site. Parent app-of-apps already watches argocd/applications/.
After sync, the Application should be Synced/Healthy, the Service ClusterIP only, and the pod Ready:
kubectl -n argocd get application jellyfin
kubectl -n jellyfin get svc jellyfin
kubectl -n jellyfin get podsService jellyfin must show type ClusterIP on port 8096 and must not have an EXTERNAL-IP.
Smoke#
v1 health is pod Ready plus the chart’s GET /health. After the hostname exists, sign in through Access at https://jellyfin.<your-domain>, then request https://jellyfin.<your-domain>/health. A 2xx after you pass Access is enough; an Access challenge before that is expected. A Cloudflare 502 means the origin Service or pod (the same signal as Immich and Grafana), so check svc/jellyfin and the Deployment, not a VIP.
There is no ServiceMonitor, PrometheusRule, Grafana dashboard, or Blackbox Probe for Jellyfin in v1. kube-prometheus-stack already watches pod Ready and the Longhorn config volume.
Rollback#
Disable or delete Application jellyfin, or scale the Deployment to 0. Config remains on jellyfin-config-pvc-lh until you delete that claim. Media files stay on the NAS (Retain). Remove the Zero Trust Public Hostname and its Access application. Leave Plex and cloudflared alone.