├── .gitignore ├── .github ├── cr.yaml ├── renovate.json └── workflows │ ├── docs_update.yaml │ ├── app_release.yaml │ └── release.yaml ├── charts └── jellyfin │ ├── .helmignore │ ├── templates │ ├── serviceaccount.yaml │ ├── httproute.yaml │ ├── ingress.yaml │ ├── serviceMonitor.yaml │ ├── service.yaml │ ├── _helpers.tpl │ ├── persistentVolumeClaim.yaml │ ├── networkpolicy.yaml │ ├── deployment.yaml │ └── NOTES.txt │ ├── tests │ ├── revision_history_test.yaml │ ├── startup_probe_test.yaml │ ├── service_ipv6_test.yaml │ ├── envfrom_test.yaml │ ├── init_containers_test.yaml │ ├── cache_persistence_test.yaml │ ├── httproute_test.yaml │ └── networkpolicy_test.yaml │ ├── MIGRATION.md │ ├── Chart.yaml │ ├── README.md.gotmpl │ ├── values.yaml │ └── README.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.github/cr.yaml: -------------------------------------------------------------------------------- 1 | owner: jellyfin 2 | release-name-template: "{{ .Name }}-{{ .Version }}" -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>jellyfin/.github//renovate-presets/default"], 4 | "customManagers": [ 5 | { 6 | "customType": "regex", 7 | "fileMatch": ["charts/jellyfin/Chart.yaml"], 8 | "matchStrings": ["appVersion: (?.*?)\\n"], 9 | "datasourceTemplate": "docker", 10 | "depNameTemplate": "jellyfin/jellyfin" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /charts/jellyfin/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "jellyfin.serviceAccountName" . }} 6 | labels: 7 | {{- include "jellyfin.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /.github/workflows/docs_update.yaml: -------------------------------------------------------------------------------- 1 | name: Docs Update 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | generate-docs: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Helm Docs GitHub Action 22 | uses: losisin/helm-docs-github-action@a57fae5676e4c55a228ea654a1bcaec8dd3cf5b5 # v1.6.2 23 | with: 24 | git-push: true 25 | git-commit-message: "docs(charts): :memo: write updated reademe" 26 | 27 | - name: Push changes 28 | run: git push -------------------------------------------------------------------------------- /charts/jellyfin/templates/httproute.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.httpRoute.enabled }} 2 | apiVersion: gateway.networking.k8s.io/v1 3 | kind: HTTPRoute 4 | metadata: 5 | name: {{ include "jellyfin.fullname" . }} 6 | labels: 7 | {{- include "jellyfin.labels" . | nindent 4 }} 8 | {{- with .Values.httpRoute.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | {{- with .Values.httpRoute.parentRefs }} 14 | parentRefs: 15 | {{- toYaml . | nindent 4 }} 16 | {{- end }} 17 | {{- with .Values.httpRoute.hostnames }} 18 | hostnames: 19 | {{- toYaml . | nindent 4 }} 20 | {{- end }} 21 | rules: 22 | {{- range .Values.httpRoute.rules }} 23 | - matches: 24 | {{- range .matches }} 25 | - path: 26 | type: {{ .path.type }} 27 | value: {{ .path.value }} 28 | {{- end }} 29 | backendRefs: 30 | - name: {{ include "jellyfin.fullname" $ }} 31 | port: {{ $.Values.service.port }} 32 | {{- end }} 33 | {{- end }} 34 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/revision_history_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test revision history limit 2 | templates: 3 | - deployment.yaml 4 | tests: 5 | - it: should set revisionHistoryLimit to 3 by default 6 | asserts: 7 | - equal: 8 | path: spec.revisionHistoryLimit 9 | value: 3 10 | 11 | - it: should allow custom revisionHistoryLimit 12 | set: 13 | revisionHistoryLimit: 5 14 | asserts: 15 | - equal: 16 | path: spec.revisionHistoryLimit 17 | value: 5 18 | 19 | - it: should allow revisionHistoryLimit of 0 20 | set: 21 | revisionHistoryLimit: 0 22 | asserts: 23 | - equal: 24 | path: spec.revisionHistoryLimit 25 | value: 0 26 | 27 | - it: should allow revisionHistoryLimit of 1 28 | set: 29 | revisionHistoryLimit: 1 30 | asserts: 31 | - equal: 32 | path: spec.revisionHistoryLimit 33 | value: 1 34 | 35 | - it: should allow large revisionHistoryLimit 36 | set: 37 | revisionHistoryLimit: 100 38 | asserts: 39 | - equal: 40 | path: spec.revisionHistoryLimit 41 | value: 100 42 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "jellyfin.fullname" . }} 6 | labels: 7 | {{- include "jellyfin.labels" . | nindent 4 }} 8 | {{- with .Values.ingress.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | {{- with .Values.ingress.className }} 14 | ingressClassName: {{ . }} 15 | {{- end }} 16 | {{- if .Values.ingress.tls }} 17 | tls: 18 | {{- range .Values.ingress.tls }} 19 | - hosts: 20 | {{- range .hosts }} 21 | - {{ . | quote }} 22 | {{- end }} 23 | secretName: {{ .secretName }} 24 | {{- end }} 25 | {{- end }} 26 | rules: 27 | {{- range .Values.ingress.hosts }} 28 | - host: {{ .host | quote }} 29 | http: 30 | paths: 31 | {{- range .paths }} 32 | - path: {{ .path }} 33 | {{- with .pathType }} 34 | pathType: {{ . }} 35 | {{- end }} 36 | backend: 37 | service: 38 | name: {{ include "jellyfin.fullname" $ }} 39 | port: 40 | number: {{ $.Values.service.port }} 41 | {{- end }} 42 | {{- end }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /charts/jellyfin/MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | ## v1.x to v2.x 4 | 5 | ### General 6 | 7 | 1. The `enableDLNA` key has been moved from the top level to the `jellyfin` subkey. 8 | 2. Some renames have occurred to align with the standard: 9 | ```yaml 10 | extraPodLabels -> podLabels 11 | extraPodAnnotations -> podAnnotations 12 | ``` 13 | 3. The `extraEnvVars` key has been moved and renamed to `jellyfin.env`. 14 | 4. `extraVolumes` has been moved to `volumes`, and `extraVolumeMounts` has been moved to `volumeMounts`. 15 | 5. The `extraExistingClaimMounts` key has been removed, as it can now be represented with `volumes` and `volumeMounts`. 16 | 17 | ### Service 18 | 19 | 1. The `name` field has been renamed to `portName` for consistency. 20 | 2. The following fields have been added: 21 | ```yaml 22 | # -- Configure dual-stack IP family policy. See: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ 23 | ipFamilyPolicy: "" 24 | # -- Supported IP families (IPv4, IPv6). 25 | ipFamilies: [] 26 | # -- Class of the LoadBalancer. 27 | loadBalancerClass: "" 28 | # -- External traffic policy (Cluster or Local). 29 | # externalTrafficPolicy: Cluster 30 | ``` 31 | 32 | ### Ingress 33 | 34 | 1. The `className` field has been added for ingress class, as annotations are deprecated. 35 | 2. The `path` field has been moved under the `hosts` key to better represent the actual CRD, providing more fine-grained control. 36 | 3. The `labels` field has been added for additional labels. 37 | 38 | ### Persistence 39 | 40 | PVC creation is now enabled by default to prevent data loss when the chart is used without a specific configuration. 41 | 42 | ### Probes 43 | 44 | The liveness and readiness probes are now always enabled to ensure proper Kubernetes lifecycle management. Adjust the values accordingly if you have a large library. -------------------------------------------------------------------------------- /charts/jellyfin/templates/serviceMonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.metrics.serviceMonitor.enabled .Values.metrics.enabled }} 2 | --- 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: {{ include "jellyfin.fullname" . }} 7 | {{- if .Values.metrics.serviceMonitor.namespace }} 8 | namespace: {{ tpl .Values.metrics.serviceMonitor.namespace . }} 9 | {{- end }} 10 | labels: 11 | {{- include "jellyfin.labels" . | nindent 4 }} 12 | {{- with .Values.metrics.serviceMonitor.labels }} 13 | {{- tpl (toYaml . | nindent 4) $ }} 14 | {{- end }} 15 | spec: 16 | endpoints: 17 | - port: {{ .Values.service.name | default "http" }} 18 | {{- with .Values.metrics.serviceMonitor.interval }} 19 | interval: {{ . }} 20 | {{- end }} 21 | {{- with .Values.metrics.serviceMonitor.scrapeTimeout }} 22 | scrapeTimeout: {{ . }} 23 | {{- end }} 24 | honorLabels: true 25 | path: {{ .Values.metrics.serviceMonitor.path }} 26 | scheme: {{ .Values.metrics.serviceMonitor.scheme }} 27 | {{- with .Values.metrics.serviceMonitor.tlsConfig }} 28 | tlsConfig: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | {{- with .Values.metrics.serviceMonitor.relabelings }} 32 | relabelings: 33 | {{- toYaml . | nindent 8 }} 34 | {{- end }} 35 | {{- with .Values.metrics.serviceMonitor.metricRelabelings }} 36 | metricRelabelings: 37 | {{- toYaml . | nindent 8 }} 38 | {{- end }} 39 | jobLabel: "{{ .Release.Name }}" 40 | selector: 41 | matchLabels: 42 | app.kubernetes.io/name: {{ include "jellyfin.name" . }} 43 | app.kubernetes.io/instance: {{ .Release.Name }} 44 | {{- with .Values.metrics.serviceMonitor.targetLabels }} 45 | targetLabels: 46 | {{- toYaml . | nindent 4 }} 47 | {{- end }} 48 | {{- end }} 49 | -------------------------------------------------------------------------------- /charts/jellyfin/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: jellyfin 3 | description: A Helm chart for Jellyfin Media Server 4 | type: application 5 | home: https://jellyfin.org/ 6 | icon: https://jellyfin.org/images/logo.svg 7 | keywords: 8 | - jellyfin 9 | - media 10 | - self-hosted 11 | version: 3.0.0 12 | appVersion: 10.11.5 13 | annotations: 14 | artifacthub.io/changes: | 15 | - kind: deprecated 16 | description: Deprecate initContainers parameter in favor of extraInitContainers for consistency (will be removed after 2030) 17 | - kind: fixed 18 | description: Fix extraInitContainers not working (was using wrong parameter name in template) 19 | - kind: added 20 | description: Add NetworkPolicy support for network isolation and security hardening 21 | links: 22 | - name: Documentation 23 | url: https://kubernetes.io/docs/concepts/services-networking/network-policies/ 24 | description: Add troubleshooting documentation for inotify instance limits with workaround example 25 | - kind: added 26 | description: Add support for Gateway API HTTPRoute resource 27 | - kind: added 28 | description: Add envFrom support to load environment variables from ConfigMap or Secret 29 | - kind: added 30 | description: Add NOTES.txt with helpful post-installation information and deprecation warnings 31 | - kind: added 32 | description: Add startup probe to prevent pod restarts during slow initial startup with large media libraries 33 | - kind: added 34 | description: Add persistence.cache configuration for dedicated cache volume support 35 | - kind: added 36 | description: Add comprehensive IPv6 and dual-stack networking documentation 37 | - kind: added 38 | description: Add revisionHistoryLimit parameter to control rollback history retention (defaults to 3 instead of Kubernetes default 10) 39 | - kind: changed 40 | description: Improve health probe configuration examples for IPv6 compatibility 41 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "jellyfin.fullname" . }} 5 | labels: 6 | {{- include "jellyfin.labels" . | nindent 4 }} 7 | {{- with .Values.service.labels }} 8 | {{- toYaml . | nindent 4 }} 9 | {{- end }} 10 | {{- with .Values.service.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | {{- if (or (eq .Values.service.type "ClusterIP") (empty .Values.service.type)) }} 16 | type: ClusterIP 17 | {{- with .Values.service.clusterIP }} 18 | clusterIP: {{ . }} 19 | {{- end }} 20 | {{- else if eq .Values.service.type "LoadBalancer" }} 21 | type: LoadBalancer 22 | {{- with .Values.service.loadBalancerIP }} 23 | loadBalancerIP: {{ . }} 24 | {{- end }} 25 | {{- with .Values.service.loadBalancerClass }} 26 | loadBalancerClass: {{ . }} 27 | {{- end }} 28 | {{- with .Values.service.loadBalancerSourceRanges }} 29 | loadBalancerSourceRanges: 30 | {{- toYaml . | nindent 4 }} 31 | {{- end }} 32 | {{- else }} 33 | type: {{ .Values.service.type }} 34 | {{- end }} 35 | {{- if .Values.service.ipFamilyPolicy }} 36 | ipFamilyPolicy: {{ .Values.service.ipFamilyPolicy }} 37 | {{- end }} 38 | {{- if .Values.service.ipFamilies }} 39 | ipFamilies: 40 | {{- toYaml .Values.service.ipFamilies | nindent 4 }} 41 | {{- end }} 42 | {{- with .Values.service.externalIPs }} 43 | externalIPs: 44 | {{- toYaml . | nindent 4 }} 45 | {{- end }} 46 | {{- with .Values.service.externalTrafficPolicy }} 47 | externalTrafficPolicy: {{ . }} 48 | {{- end }} 49 | ports: 50 | - name: {{ .Values.service.name | default "http" }} 51 | port: {{ .Values.service.port }} 52 | protocol: TCP 53 | targetPort: http 54 | {{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} 55 | nodePort: {{ .Values.service.nodePort }} 56 | {{- end }} 57 | selector: 58 | {{- include "jellyfin.selectorLabels" . | nindent 4 }} 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jellyfin Helm Charts Repository 2 | 3 | This repository contains Helm charts for the jellyfin project. 4 | 5 | ## Repository Structure 6 | 7 | Each Helm chart is stored in its own directory. Each chart follows the standard Helm chart structure: 8 | 9 | ```plaintext 10 | jellyfin-helm/ 11 | ├── jellyfin/ 12 | │ ├── Chart.yaml 13 | │ ├── values.yaml 14 | │ ├── templates/ 15 | │ └── ... 16 | ├── chart2/ 17 | │ ├── Chart.yaml 18 | │ ├── values.yaml 19 | │ ├── templates/ 20 | │ └── ... 21 | └── ... 22 | ``` 23 | 24 | - `Chart.yaml`: Contains metadata about the chart (name, version, app version, etc.). 25 | - `values.yaml`: Default configuration values for the chart. 26 | - `templates/`: The Kubernetes resource templates (YAML files) that Helm uses to deploy the chart. 27 | 28 | ## How to Use the Charts 29 | 30 | ### 1. Add this Repository 31 | 32 | To use the charts in this repository, first add the repository to Helm: 33 | 34 | ```bash 35 | helm repo add jellyfin https://jellyfin.github.io/jellyfin-helm 36 | helm repo update 37 | ``` 38 | 39 | ### 2. Install a Chart 40 | 41 | To install a chart from this repository, use the following command: 42 | 43 | ```bash 44 | helm install jellyfin/ 45 | ``` 46 | 47 | ### 3. Customize Installation 48 | 49 | You can customize the installation by passing in custom values via the `--set` flag or by using a `values.yaml` file: 50 | 51 | ```bash 52 | helm install jellyfin/ --set key=value 53 | ``` 54 | 55 | or 56 | 57 | ```bash 58 | helm install jellyfin/ -f custom-values.yaml 59 | ``` 60 | 61 | ## Contributing 62 | 63 | Feel free to contribute to this repository by: 64 | 65 | - Submitting new charts. 66 | - Fixing bugs or issues. 67 | - Improving documentation. 68 | 69 | To contribute, fork this repository, create a new branch, and submit a pull request with your changes. 70 | 71 | ## License 72 | 73 | This repository is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. 74 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Fail if command and metrics are set 3 | */}} 4 | {{- if and .Values.image.command .Values.metrics.enabled }} 5 | {{- fail "Can't use custom command and metrics in combination. They are exclusive features" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Expand the name of the chart. 10 | */}} 11 | {{- define "jellyfin.name" -}} 12 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 13 | {{- end }} 14 | 15 | {{/* 16 | Create a default fully qualified app name. 17 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 18 | If release name contains chart name it will be used as a full name. 19 | */}} 20 | {{- define "jellyfin.fullname" -}} 21 | {{- if .Values.fullnameOverride }} 22 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 23 | {{- else }} 24 | {{- $name := default .Chart.Name .Values.nameOverride }} 25 | {{- if contains $name .Release.Name }} 26 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 27 | {{- else }} 28 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 29 | {{- end }} 30 | {{- end }} 31 | {{- end }} 32 | 33 | {{/* 34 | Create chart name and version as used by the chart label. 35 | */}} 36 | {{- define "jellyfin.chart" -}} 37 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 38 | {{- end }} 39 | 40 | {{/* 41 | Common labels 42 | */}} 43 | {{- define "jellyfin.labels" -}} 44 | helm.sh/chart: {{ include "jellyfin.chart" . }} 45 | {{ include "jellyfin.selectorLabels" . }} 46 | {{- if .Chart.AppVersion }} 47 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 48 | {{- end }} 49 | app.kubernetes.io/managed-by: {{ .Release.Service }} 50 | {{- end }} 51 | 52 | {{/* 53 | Selector labels 54 | */}} 55 | {{- define "jellyfin.selectorLabels" -}} 56 | app.kubernetes.io/name: {{ include "jellyfin.name" . }} 57 | app.kubernetes.io/instance: {{ .Release.Name }} 58 | {{- end }} 59 | 60 | {{/* 61 | Create the name of the service account to use 62 | */}} 63 | {{- define "jellyfin.serviceAccountName" -}} 64 | {{- if .Values.serviceAccount.create }} 65 | {{- default (include "jellyfin.fullname" .) .Values.serviceAccount.name }} 66 | {{- else }} 67 | {{- default "default" .Values.serviceAccount.name }} 68 | {{- end }} 69 | {{- end }} 70 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/persistentVolumeClaim.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.persistence.config.enabled (not .Values.persistence.config.existingClaim) }} 2 | --- 3 | apiVersion: v1 4 | kind: PersistentVolumeClaim 5 | metadata: 6 | name: {{ include "jellyfin.fullname" . }}-config 7 | labels: 8 | {{- include "jellyfin.labels" . | nindent 4 }} 9 | {{- with .Values.persistence.config.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | accessModes: 15 | - {{ .Values.persistence.config.accessMode | quote }} 16 | resources: 17 | requests: 18 | storage: {{ .Values.persistence.config.size | quote }} 19 | {{- with .Values.persistence.config.storageClass }} 20 | storageClassName: {{ . | quote }} 21 | {{- end }} 22 | {{- end }} 23 | 24 | {{- if and .Values.persistence.media.enabled (eq .Values.persistence.media.type "pvc") (not .Values.persistence.media.existingClaim) }} 25 | --- 26 | apiVersion: v1 27 | kind: PersistentVolumeClaim 28 | metadata: 29 | name: {{ include "jellyfin.fullname" . }}-media 30 | labels: 31 | {{- include "jellyfin.labels" . | nindent 4 }} 32 | {{- with .Values.persistence.media.annotations }} 33 | annotations: 34 | {{- toYaml . | nindent 4 }} 35 | {{- end }} 36 | spec: 37 | accessModes: 38 | - {{ .Values.persistence.media.accessMode | quote }} 39 | resources: 40 | requests: 41 | storage: {{ .Values.persistence.media.size | quote }} 42 | {{- with .Values.persistence.media.storageClass }} 43 | storageClassName: {{ . | quote }} 44 | {{- end }} 45 | {{- end }} 46 | 47 | {{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "pvc") (not .Values.persistence.cache.existingClaim) }} 48 | --- 49 | apiVersion: v1 50 | kind: PersistentVolumeClaim 51 | metadata: 52 | name: {{ include "jellyfin.fullname" . }}-cache 53 | labels: 54 | {{- include "jellyfin.labels" . | nindent 4 }} 55 | {{- with .Values.persistence.cache.annotations }} 56 | annotations: 57 | {{- toYaml . | nindent 4 }} 58 | {{- end }} 59 | spec: 60 | accessModes: 61 | - {{ .Values.persistence.cache.accessMode | quote }} 62 | resources: 63 | requests: 64 | storage: {{ .Values.persistence.cache.size | quote }} 65 | {{- with .Values.persistence.cache.storageClass }} 66 | storageClassName: {{ . | quote }} 67 | {{- end }} 68 | {{- end }} 69 | -------------------------------------------------------------------------------- /.github/workflows/app_release.yaml: -------------------------------------------------------------------------------- 1 | name: Bump Helm Chart Version 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | jellyfin_version: 10 | description: 'Application version to set in Chart.yaml' 11 | required: true 12 | default: '1.0.0' 13 | 14 | jobs: 15 | bump-version: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 21 | with: 22 | persist-credentials: false 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 26 | with: 27 | python-version: 3.x 28 | 29 | - name: Bump chart version and set appVersion 30 | id: bump_version 31 | run: | 32 | # Read the current version from the Chart.yaml 33 | current_version=$(grep '^version:' charts/jellyfin/Chart.yaml | awk '{print $2}') 34 | echo "Current version: $current_version" 35 | 36 | # Split the version by dot and increment the minor version 37 | major=$(echo "$current_version" | cut -d '.' -f 1) 38 | minor=$(echo "$current_version" | cut -d '.' -f 2) 39 | patch=$(echo "$current_version" | cut -d '.' -f 3) 40 | new_version="$major.$((minor+1)).0" 41 | 42 | echo "New chart version: $new_version" 43 | 44 | # Get the app version passed as an input 45 | jellyfin_version="${{ github.event.inputs.jellyfin_version }}" 46 | echo "Setting appVersion to: $jellyfin_version" 47 | 48 | # Update the Chart.yaml with the new version and appVersion 49 | sed -i "s/^version:.*/version: $new_version/" charts/jellyfin/Chart.yaml 50 | sed -i "s/^appVersion:.*/appVersion: $jellyfin_version/" charts/jellyfin/Chart.yaml 51 | 52 | # Output the new chart version for further use 53 | echo "new_version=$new_version" >> $GITHUB_OUTPUT 54 | 55 | - name: Commit changes 56 | run: | 57 | git config --global user.name "github-actions[bot]" 58 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 59 | git add charts/jellyfin/Chart.yaml 60 | git commit -m "chore(jellyfin): Bump chart version to ${{ steps.bump_version.outputs.new_version }} and set appVersion to ${{ github.event.inputs.jellyfin_version }}" 61 | 62 | - name: Push changes 63 | uses: ad-m/github-push-action@master 64 | with: 65 | github_token: ${{ secrets.JF_BOT_TOKEN }} 66 | 67 | - name: Create Git tag 68 | id: create_tag 69 | run: | 70 | new_version="${{ steps.bump_version.outputs.new_version }}" 71 | git tag "$new_version" 72 | echo "Tag created: $new_version" 73 | 74 | - name: Push changes 75 | uses: ad-m/github-push-action@master 76 | with: 77 | github_token: ${{ secrets.JF_BOT_TOKEN }} 78 | tags: true 79 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/startup_probe_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test startup probe 2 | templates: 3 | - deployment.yaml 4 | tests: 5 | - it: should have startup probe by default 6 | asserts: 7 | - isNotNull: 8 | path: spec.template.spec.containers[0].startupProbe 9 | 10 | - it: should use tcpSocket by default 11 | asserts: 12 | - isNotNull: 13 | path: spec.template.spec.containers[0].startupProbe.tcpSocket 14 | - equal: 15 | path: spec.template.spec.containers[0].startupProbe.tcpSocket.port 16 | value: http 17 | 18 | - it: should have default timing values 19 | asserts: 20 | - equal: 21 | path: spec.template.spec.containers[0].startupProbe.initialDelaySeconds 22 | value: 0 23 | - equal: 24 | path: spec.template.spec.containers[0].startupProbe.periodSeconds 25 | value: 10 26 | - equal: 27 | path: spec.template.spec.containers[0].startupProbe.failureThreshold 28 | value: 30 29 | 30 | - it: should allow custom initialDelaySeconds 31 | set: 32 | startupProbe.initialDelaySeconds: 5 33 | asserts: 34 | - equal: 35 | path: spec.template.spec.containers[0].startupProbe.initialDelaySeconds 36 | value: 5 37 | 38 | - it: should allow custom periodSeconds 39 | set: 40 | startupProbe.periodSeconds: 15 41 | asserts: 42 | - equal: 43 | path: spec.template.spec.containers[0].startupProbe.periodSeconds 44 | value: 15 45 | 46 | - it: should allow custom failureThreshold 47 | set: 48 | startupProbe.failureThreshold: 60 49 | asserts: 50 | - equal: 51 | path: spec.template.spec.containers[0].startupProbe.failureThreshold 52 | value: 60 53 | 54 | - it: should support httpGet probe 55 | set: 56 | startupProbe: 57 | httpGet: 58 | path: /health 59 | port: http 60 | initialDelaySeconds: 0 61 | periodSeconds: 10 62 | failureThreshold: 30 63 | asserts: 64 | - isNotNull: 65 | path: spec.template.spec.containers[0].startupProbe.httpGet 66 | - equal: 67 | path: spec.template.spec.containers[0].startupProbe.httpGet.path 68 | value: /health 69 | - equal: 70 | path: spec.template.spec.containers[0].startupProbe.httpGet.port 71 | value: http 72 | 73 | - it: should support custom port 74 | set: 75 | startupProbe.tcpSocket.port: 9000 76 | asserts: 77 | - equal: 78 | path: spec.template.spec.containers[0].startupProbe.tcpSocket.port 79 | value: 9000 80 | 81 | - it: should calculate correct timeout window 82 | set: 83 | startupProbe.periodSeconds: 10 84 | startupProbe.failureThreshold: 30 85 | asserts: 86 | - equal: 87 | path: spec.template.spec.containers[0].startupProbe.periodSeconds 88 | value: 10 89 | - equal: 90 | path: spec.template.spec.containers[0].startupProbe.failureThreshold 91 | value: 30 92 | # Total startup time allowed: 10 * 30 = 300 seconds (5 minutes) 93 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/service_ipv6_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test service ipv6 and dual-stack 2 | templates: 3 | - service.yaml 4 | tests: 5 | - it: should not set ipFamilyPolicy by default 6 | asserts: 7 | - isNull: 8 | path: spec.ipFamilyPolicy 9 | 10 | - it: should not set ipFamilies by default 11 | asserts: 12 | - isNull: 13 | path: spec.ipFamilies 14 | 15 | - it: should set ipFamilyPolicy when specified 16 | set: 17 | service.ipFamilyPolicy: PreferDualStack 18 | asserts: 19 | - equal: 20 | path: spec.ipFamilyPolicy 21 | value: PreferDualStack 22 | 23 | - it: should support SingleStack policy 24 | set: 25 | service.ipFamilyPolicy: SingleStack 26 | asserts: 27 | - equal: 28 | path: spec.ipFamilyPolicy 29 | value: SingleStack 30 | 31 | - it: should support RequireDualStack policy 32 | set: 33 | service.ipFamilyPolicy: RequireDualStack 34 | asserts: 35 | - equal: 36 | path: spec.ipFamilyPolicy 37 | value: RequireDualStack 38 | 39 | - it: should set ipFamilies for IPv4 only 40 | set: 41 | service.ipFamilies: 42 | - IPv4 43 | asserts: 44 | - equal: 45 | path: spec.ipFamilies[0] 46 | value: IPv4 47 | 48 | - it: should set ipFamilies for IPv6 only 49 | set: 50 | service.ipFamilies: 51 | - IPv6 52 | asserts: 53 | - equal: 54 | path: spec.ipFamilies[0] 55 | value: IPv6 56 | 57 | - it: should set ipFamilies for dual-stack IPv4 primary 58 | set: 59 | service.ipFamilies: 60 | - IPv4 61 | - IPv6 62 | asserts: 63 | - equal: 64 | path: spec.ipFamilies[0] 65 | value: IPv4 66 | - equal: 67 | path: spec.ipFamilies[1] 68 | value: IPv6 69 | 70 | - it: should set ipFamilies for dual-stack IPv6 primary 71 | set: 72 | service.ipFamilies: 73 | - IPv6 74 | - IPv4 75 | asserts: 76 | - equal: 77 | path: spec.ipFamilies[0] 78 | value: IPv6 79 | - equal: 80 | path: spec.ipFamilies[1] 81 | value: IPv4 82 | 83 | - it: should work with PreferDualStack and both families 84 | set: 85 | service.ipFamilyPolicy: PreferDualStack 86 | service.ipFamilies: 87 | - IPv4 88 | - IPv6 89 | asserts: 90 | - equal: 91 | path: spec.ipFamilyPolicy 92 | value: PreferDualStack 93 | - equal: 94 | path: spec.ipFamilies[0] 95 | value: IPv4 96 | - equal: 97 | path: spec.ipFamilies[1] 98 | value: IPv6 99 | 100 | - it: should work with RequireDualStack and both families 101 | set: 102 | service.ipFamilyPolicy: RequireDualStack 103 | service.ipFamilies: 104 | - IPv6 105 | - IPv4 106 | asserts: 107 | - equal: 108 | path: spec.ipFamilyPolicy 109 | value: RequireDualStack 110 | - equal: 111 | path: spec.ipFamilies[0] 112 | value: IPv6 113 | - equal: 114 | path: spec.ipFamilies[1] 115 | value: IPv4 116 | 117 | - it: should work with SingleStack IPv6 only 118 | set: 119 | service.ipFamilyPolicy: SingleStack 120 | service.ipFamilies: 121 | - IPv6 122 | asserts: 123 | - equal: 124 | path: spec.ipFamilyPolicy 125 | value: SingleStack 126 | - equal: 127 | path: spec.ipFamilies[0] 128 | value: IPv6 129 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/envfrom_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test envFrom 2 | templates: 3 | - deployment.yaml 4 | tests: 5 | - it: should not have envFrom by default 6 | asserts: 7 | - isNull: 8 | path: spec.template.spec.containers[0].envFrom 9 | 10 | - it: should add envFrom from ConfigMap 11 | set: 12 | jellyfin.envFrom: 13 | - configMapRef: 14 | name: jellyfin-config 15 | asserts: 16 | - isNotNull: 17 | path: spec.template.spec.containers[0].envFrom 18 | - lengthEqual: 19 | path: spec.template.spec.containers[0].envFrom 20 | count: 1 21 | - equal: 22 | path: spec.template.spec.containers[0].envFrom[0].configMapRef.name 23 | value: jellyfin-config 24 | 25 | - it: should add envFrom from Secret 26 | set: 27 | jellyfin.envFrom: 28 | - secretRef: 29 | name: jellyfin-secrets 30 | asserts: 31 | - equal: 32 | path: spec.template.spec.containers[0].envFrom[0].secretRef.name 33 | value: jellyfin-secrets 34 | 35 | - it: should support multiple envFrom sources 36 | set: 37 | jellyfin.envFrom: 38 | - configMapRef: 39 | name: jellyfin-config 40 | - secretRef: 41 | name: jellyfin-secrets 42 | - configMapRef: 43 | name: jellyfin-extra-config 44 | asserts: 45 | - lengthEqual: 46 | path: spec.template.spec.containers[0].envFrom 47 | count: 3 48 | - equal: 49 | path: spec.template.spec.containers[0].envFrom[0].configMapRef.name 50 | value: jellyfin-config 51 | - equal: 52 | path: spec.template.spec.containers[0].envFrom[1].secretRef.name 53 | value: jellyfin-secrets 54 | - equal: 55 | path: spec.template.spec.containers[0].envFrom[2].configMapRef.name 56 | value: jellyfin-extra-config 57 | 58 | - it: should support optional ConfigMap 59 | set: 60 | jellyfin.envFrom: 61 | - configMapRef: 62 | name: optional-config 63 | optional: true 64 | asserts: 65 | - equal: 66 | path: spec.template.spec.containers[0].envFrom[0].configMapRef.optional 67 | value: true 68 | 69 | - it: should support optional Secret 70 | set: 71 | jellyfin.envFrom: 72 | - secretRef: 73 | name: optional-secret 74 | optional: true 75 | asserts: 76 | - equal: 77 | path: spec.template.spec.containers[0].envFrom[0].secretRef.optional 78 | value: true 79 | 80 | - it: should support prefix for ConfigMap 81 | set: 82 | jellyfin.envFrom: 83 | - configMapRef: 84 | name: my-config 85 | prefix: CONFIG_ 86 | asserts: 87 | - equal: 88 | path: spec.template.spec.containers[0].envFrom[0].prefix 89 | value: CONFIG_ 90 | 91 | - it: should support prefix for Secret 92 | set: 93 | jellyfin.envFrom: 94 | - secretRef: 95 | name: my-secret 96 | prefix: SECRET_ 97 | asserts: 98 | - equal: 99 | path: spec.template.spec.containers[0].envFrom[0].prefix 100 | value: SECRET_ 101 | 102 | - it: should work alongside regular env variables 103 | set: 104 | jellyfin.env: 105 | - name: CUSTOM_VAR 106 | value: custom-value 107 | jellyfin.envFrom: 108 | - configMapRef: 109 | name: jellyfin-config 110 | asserts: 111 | - isNotNull: 112 | path: spec.template.spec.containers[0].env 113 | - isNotNull: 114 | path: spec.template.spec.containers[0].envFrom 115 | - contains: 116 | path: spec.template.spec.containers[0].env 117 | content: 118 | name: CUSTOM_VAR 119 | value: custom-value 120 | - equal: 121 | path: spec.template.spec.containers[0].envFrom[0].configMapRef.name 122 | value: jellyfin-config 123 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/init_containers_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test init containers 2 | templates: 3 | - deployment.yaml 4 | tests: 5 | - it: should not have init containers by default 6 | asserts: 7 | - isNull: 8 | path: spec.template.spec.initContainers 9 | 10 | - it: should add extraInitContainers when specified 11 | set: 12 | extraInitContainers: 13 | - name: init-config 14 | image: busybox:1.35 15 | command: ['sh', '-c', 'echo Initializing'] 16 | asserts: 17 | - isNotNull: 18 | path: spec.template.spec.initContainers 19 | - lengthEqual: 20 | path: spec.template.spec.initContainers 21 | count: 1 22 | - equal: 23 | path: spec.template.spec.initContainers[0].name 24 | value: init-config 25 | - equal: 26 | path: spec.template.spec.initContainers[0].image 27 | value: busybox:1.35 28 | 29 | - it: should support multiple extraInitContainers 30 | set: 31 | extraInitContainers: 32 | - name: init-1 33 | image: busybox:1.35 34 | - name: init-2 35 | image: alpine:3.18 36 | asserts: 37 | - lengthEqual: 38 | path: spec.template.spec.initContainers 39 | count: 2 40 | - equal: 41 | path: spec.template.spec.initContainers[0].name 42 | value: init-1 43 | - equal: 44 | path: spec.template.spec.initContainers[1].name 45 | value: init-2 46 | 47 | - it: should preserve full container spec in extraInitContainers 48 | set: 49 | extraInitContainers: 50 | - name: init-permissions 51 | image: busybox:1.35 52 | command: ['sh', '-c'] 53 | args: ['chown -R 1000:1000 /config'] 54 | volumeMounts: 55 | - name: config 56 | mountPath: /config 57 | securityContext: 58 | runAsUser: 0 59 | asserts: 60 | - equal: 61 | path: spec.template.spec.initContainers[0].command[0] 62 | value: sh 63 | - equal: 64 | path: spec.template.spec.initContainers[0].args[0] 65 | value: 'chown -R 1000:1000 /config' 66 | - equal: 67 | path: spec.template.spec.initContainers[0].volumeMounts[0].name 68 | value: config 69 | - equal: 70 | path: spec.template.spec.initContainers[0].securityContext.runAsUser 71 | value: 0 72 | 73 | # Deprecated initContainers (backward compatibility) 74 | - it: should still support deprecated initContainers parameter 75 | set: 76 | initContainers: 77 | - name: legacy-init 78 | image: busybox:1.35 79 | asserts: 80 | - lengthEqual: 81 | path: spec.template.spec.initContainers 82 | count: 1 83 | - equal: 84 | path: spec.template.spec.initContainers[0].name 85 | value: legacy-init 86 | 87 | - it: should merge initContainers and extraInitContainers 88 | set: 89 | initContainers: 90 | - name: legacy-init 91 | image: busybox:1.35 92 | extraInitContainers: 93 | - name: new-init 94 | image: alpine:3.18 95 | asserts: 96 | - lengthEqual: 97 | path: spec.template.spec.initContainers 98 | count: 2 99 | - equal: 100 | path: spec.template.spec.initContainers[0].name 101 | value: legacy-init 102 | - equal: 103 | path: spec.template.spec.initContainers[1].name 104 | value: new-init 105 | 106 | - it: should prioritize extraInitContainers over initContainers 107 | set: 108 | initContainers: 109 | - name: old-way 110 | image: busybox:1.35 111 | extraInitContainers: 112 | - name: new-way 113 | image: alpine:3.18 114 | asserts: 115 | - contains: 116 | path: spec.template.spec.initContainers 117 | content: 118 | name: new-way 119 | image: alpine:3.18 120 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.networkPolicy.enabled }} 2 | {{- if or .Values.jellyfin.enableDLNA .Values.podPrivileges.hostNetwork }} 3 | {{- fail "NetworkPolicy cannot be enabled when hostNetwork is enabled (DLNA mode or podPrivileges.hostNetwork). NetworkPolicy does not apply to pods using hostNetwork. Please set networkPolicy.enabled=false or disable hostNetwork." }} 4 | {{- end }} 5 | --- 6 | apiVersion: networking.k8s.io/v1 7 | kind: NetworkPolicy 8 | metadata: 9 | name: {{ include "jellyfin.fullname" . }} 10 | labels: 11 | {{- include "jellyfin.labels" . | nindent 4 }} 12 | spec: 13 | podSelector: 14 | matchLabels: 15 | {{- include "jellyfin.selectorLabels" . | nindent 6 }} 16 | policyTypes: 17 | {{- toYaml .Values.networkPolicy.policyTypes | nindent 4 }} 18 | 19 | {{- if has "Ingress" .Values.networkPolicy.policyTypes }} 20 | ingress: 21 | # Allow traffic to Jellyfin HTTP port 22 | - ports: 23 | - protocol: TCP 24 | port: http 25 | {{- if not .Values.networkPolicy.ingress.allowExternal }} 26 | from: 27 | {{- if or .Values.networkPolicy.ingress.podSelector .Values.networkPolicy.ingress.namespaceSelector }} 28 | - podSelector: 29 | {{- toYaml .Values.networkPolicy.ingress.podSelector | nindent 12 }} 30 | {{- if .Values.networkPolicy.ingress.namespaceSelector }} 31 | namespaceSelector: 32 | {{- toYaml .Values.networkPolicy.ingress.namespaceSelector | nindent 12 }} 33 | {{- end }} 34 | {{- end }} 35 | {{- end }} 36 | 37 | {{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} 38 | # Allow Prometheus metrics scraping 39 | - ports: 40 | - protocol: TCP 41 | port: {{ .Values.metrics.serviceMonitor.port }} 42 | from: 43 | - podSelector: 44 | {{- toYaml .Values.networkPolicy.metrics.podSelector | nindent 12 }} 45 | {{- if .Values.networkPolicy.metrics.namespace }} 46 | namespaceSelector: 47 | matchLabels: 48 | kubernetes.io/metadata.name: {{ .Values.networkPolicy.metrics.namespace }} 49 | {{- end }} 50 | {{- end }} 51 | 52 | {{- with .Values.networkPolicy.ingress.customRules }} 53 | {{- toYaml . | nindent 4 }} 54 | {{- end }} 55 | {{- end }} 56 | 57 | {{- if has "Egress" .Values.networkPolicy.policyTypes }} 58 | egress: 59 | {{- if .Values.networkPolicy.egress.allowDNS }} 60 | # Allow DNS resolution (required for Jellyfin operation) 61 | - to: 62 | - namespaceSelector: 63 | matchLabels: 64 | kubernetes.io/metadata.name: {{ .Values.networkPolicy.egress.dnsNamespace }} 65 | podSelector: 66 | matchLabels: 67 | {{- toYaml .Values.networkPolicy.egress.dnsPodSelector | nindent 14 }} 68 | ports: 69 | - protocol: UDP 70 | port: 53 71 | - protocol: TCP 72 | port: 53 73 | {{- end }} 74 | 75 | {{- if .Values.networkPolicy.egress.allowAllEgress }} 76 | # Allow all egress traffic (metadata providers, subtitles, images, etc.) 77 | - to: 78 | - ipBlock: 79 | cidr: 0.0.0.0/0 80 | {{- else }} 81 | {{- if .Values.networkPolicy.egress.restrictedEgress.allowMetadata }} 82 | # Allow HTTPS for metadata providers (TMDB, TheTVDB, OpenSubtitles, etc.) 83 | - to: 84 | - ipBlock: 85 | cidr: 0.0.0.0/0 86 | ports: 87 | - protocol: TCP 88 | port: 443 89 | {{- end }} 90 | 91 | {{- if .Values.networkPolicy.egress.restrictedEgress.allowInCluster }} 92 | # Allow pod-to-pod communication within the cluster 93 | - to: 94 | - podSelector: {} 95 | {{- end }} 96 | 97 | {{- range .Values.networkPolicy.egress.restrictedEgress.allowedCIDRs }} 98 | # Custom CIDR egress rule 99 | - to: 100 | - ipBlock: 101 | cidr: {{ . }} 102 | {{- end }} 103 | {{- end }} 104 | 105 | {{- with .Values.networkPolicy.egress.customRules }} 106 | {{- toYaml . | nindent 4 }} 107 | {{- end }} 108 | {{- end }} 109 | {{- end }} 110 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/cache_persistence_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test cache persistence 2 | templates: 3 | - deployment.yaml 4 | - persistentVolumeClaim.yaml 5 | tests: 6 | # Deployment volume mount tests 7 | - it: should mount cache as emptyDir by default 8 | template: deployment.yaml 9 | asserts: 10 | - contains: 11 | path: spec.template.spec.volumes 12 | content: 13 | name: cache 14 | emptyDir: {} 15 | 16 | - it: should mount cache PVC when enabled 17 | template: deployment.yaml 18 | set: 19 | persistence.cache.enabled: true 20 | persistence.cache.type: pvc 21 | asserts: 22 | - contains: 23 | path: spec.template.spec.volumes 24 | content: 25 | name: cache 26 | persistentVolumeClaim: 27 | claimName: RELEASE-NAME-jellyfin-cache 28 | 29 | - it: should mount cache from hostPath when configured 30 | template: deployment.yaml 31 | set: 32 | persistence.cache.enabled: true 33 | persistence.cache.type: hostPath 34 | persistence.cache.hostPath: /mnt/jellyfin-cache 35 | asserts: 36 | - contains: 37 | path: spec.template.spec.volumes 38 | content: 39 | name: cache 40 | hostPath: 41 | path: /mnt/jellyfin-cache 42 | 43 | - it: should mount cache at correct path 44 | template: deployment.yaml 45 | asserts: 46 | - contains: 47 | path: spec.template.spec.containers[0].volumeMounts 48 | content: 49 | name: cache 50 | mountPath: /cache 51 | 52 | # PVC tests 53 | - it: should not create cache PVC by default 54 | template: persistentVolumeClaim.yaml 55 | asserts: 56 | - hasDocuments: 57 | count: 2 # config and media only 58 | 59 | - it: should create cache PVC when enabled 60 | template: persistentVolumeClaim.yaml 61 | set: 62 | persistence.cache.enabled: true 63 | persistence.cache.type: pvc 64 | asserts: 65 | - hasDocuments: 66 | count: 3 # config, media, and cache 67 | - contains: 68 | path: metadata.name 69 | content: RELEASE-NAME-jellyfin-cache 70 | 71 | - it: should not create cache PVC when type is hostPath 72 | template: persistentVolumeClaim.yaml 73 | set: 74 | persistence.cache.enabled: true 75 | persistence.cache.type: hostPath 76 | asserts: 77 | - hasDocuments: 78 | count: 2 # config and media only 79 | 80 | - it: should not create cache PVC when type is emptyDir 81 | template: persistentVolumeClaim.yaml 82 | set: 83 | persistence.cache.enabled: true 84 | persistence.cache.type: emptyDir 85 | asserts: 86 | - hasDocuments: 87 | count: 2 88 | 89 | - it: should set cache PVC size correctly 90 | template: persistentVolumeClaim.yaml 91 | documentIndex: 2 92 | set: 93 | persistence.cache.enabled: true 94 | persistence.cache.type: pvc 95 | persistence.cache.size: 20Gi 96 | asserts: 97 | - equal: 98 | path: spec.resources.requests.storage 99 | value: 20Gi 100 | 101 | - it: should set cache PVC access mode 102 | template: persistentVolumeClaim.yaml 103 | documentIndex: 2 104 | set: 105 | persistence.cache.enabled: true 106 | persistence.cache.type: pvc 107 | persistence.cache.accessMode: ReadWriteMany 108 | asserts: 109 | - contains: 110 | path: spec.accessModes 111 | content: ReadWriteMany 112 | 113 | - it: should set cache PVC storage class 114 | template: persistentVolumeClaim.yaml 115 | documentIndex: 2 116 | set: 117 | persistence.cache.enabled: true 118 | persistence.cache.type: pvc 119 | persistence.cache.storageClass: fast-ssd 120 | asserts: 121 | - equal: 122 | path: spec.storageClassName 123 | value: fast-ssd 124 | 125 | - it: should add annotations to cache PVC 126 | template: persistentVolumeClaim.yaml 127 | documentIndex: 2 128 | set: 129 | persistence.cache.enabled: true 130 | persistence.cache.type: pvc 131 | persistence.cache.annotations: 132 | my-annotation: my-value 133 | asserts: 134 | - equal: 135 | path: metadata.annotations.my-annotation 136 | value: my-value 137 | 138 | - it: should use existing claim when specified 139 | template: deployment.yaml 140 | set: 141 | persistence.cache.enabled: true 142 | persistence.cache.type: pvc 143 | persistence.cache.existingClaim: my-existing-cache-claim 144 | asserts: 145 | - contains: 146 | path: spec.template.spec.volumes 147 | content: 148 | name: cache 149 | persistentVolumeClaim: 150 | claimName: my-existing-cache-claim 151 | 152 | - it: should not create PVC when existing claim used 153 | template: persistentVolumeClaim.yaml 154 | set: 155 | persistence.cache.enabled: true 156 | persistence.cache.type: pvc 157 | persistence.cache.existingClaim: my-existing-cache-claim 158 | asserts: 159 | - hasDocuments: 160 | count: 2 # config and media only, not cache 161 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | release: 14 | # depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions 15 | # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token 16 | permissions: 17 | contents: write 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Configure Git 26 | run: | 27 | git config --global user.name "jellyfin-bot" 28 | git config --global user.email "team@jellyfin.org" 29 | 30 | - name: Install Helm 31 | uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 32 | env: 33 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 34 | 35 | - name: Install yq 36 | run: | 37 | # renovate: datasource=github-releases depName=mikefarah/yq 38 | YQ_VERSION="4.44.3" 39 | wget -q https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64 -O yq 40 | chmod +x yq 41 | sudo mv yq /usr/local/bin/yq 42 | yq --version 43 | 44 | - name: Run chart-releaser 45 | uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 46 | env: 47 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 48 | with: 49 | config: .github/cr.yaml 50 | 51 | - name: Extract chart metadata and format release notes 52 | id: release-notes 53 | run: | 54 | CHART_PATH="charts/jellyfin" 55 | CHART_NAME=$(yq eval '.name' "${CHART_PATH}/Chart.yaml") 56 | CHART_VERSION=$(yq eval '.version' "${CHART_PATH}/Chart.yaml") 57 | APP_VERSION=$(yq eval '.appVersion' "${CHART_PATH}/Chart.yaml") 58 | RELEASE_TAG="${CHART_NAME}-${CHART_VERSION}" 59 | 60 | echo "tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT 61 | 62 | CHANGELOG_RAW=$(yq eval '.annotations."artifacthub.io/changes"' "${CHART_PATH}/Chart.yaml") 63 | 64 | # Build changelog section 65 | CHANGELOG_SECTION="" 66 | if [ "$CHANGELOG_RAW" != "null" ] && [ -n "$CHANGELOG_RAW" ]; then 67 | CHANGELOG_SECTION="## What's Changed 68 | 69 | " 70 | TEMP_FILE=$(mktemp) 71 | SEPARATOR="|" 72 | echo "$CHANGELOG_RAW" | yq eval ".[] | .kind + \"${SEPARATOR}\" + .description" - > "$TEMP_FILE" 73 | 74 | while IFS="${SEPARATOR}" read -r kind description; do 75 | case "$kind" in 76 | "added") 77 | emoji="✨" 78 | label="Added" 79 | ;; 80 | "changed") 81 | emoji="🔄" 82 | label="Changed" 83 | ;; 84 | "deprecated") 85 | emoji="⚠️" 86 | label="Deprecated" 87 | ;; 88 | "removed") 89 | emoji="🗑️" 90 | label="Removed" 91 | ;; 92 | "fixed") 93 | emoji="🐛" 94 | label="Fixed" 95 | ;; 96 | "security") 97 | emoji="🔒" 98 | label="Security" 99 | ;; 100 | *) 101 | emoji="📝" 102 | label="Changed" 103 | ;; 104 | esac 105 | CHANGELOG_SECTION="${CHANGELOG_SECTION}- ${emoji} **${label}**: ${description} 106 | " 107 | done < "$TEMP_FILE" 108 | 109 | rm -f "$TEMP_FILE" 110 | CHANGELOG_SECTION="${CHANGELOG_SECTION} 111 | " 112 | fi 113 | 114 | # Build release body 115 | RELEASE_BODY="## Jellyfin Helm Chart v${CHART_VERSION} 116 | 117 | **Application Version**: \`${APP_VERSION}\` 118 | 119 | ${CHANGELOG_SECTION}## Installation 120 | 121 | Add the Jellyfin Helm repository: 122 | 123 | \`\`\`bash 124 | helm repo add jellyfin https://jellyfin.github.io/jellyfin-helm 125 | helm repo update 126 | \`\`\` 127 | 128 | Install the chart: 129 | 130 | \`\`\`bash 131 | helm install my-jellyfin jellyfin/jellyfin --version ${CHART_VERSION} 132 | \`\`\` 133 | 134 | ## Upgrade 135 | 136 | \`\`\`bash 137 | helm upgrade my-jellyfin jellyfin/jellyfin --version ${CHART_VERSION} 138 | \`\`\` 139 | 140 | --- 141 | 142 | 📦 **Chart**: \`jellyfin\` v${CHART_VERSION} 143 | 🎬 **Jellyfin**: v${APP_VERSION} 144 | 📖 **Documentation**: https://github.com/jellyfin/jellyfin-helm/blob/main/charts/jellyfin/README.md 145 | " 146 | 147 | # Save to file 148 | echo "$RELEASE_BODY" > release-notes.md 149 | 150 | - name: Update release notes 151 | env: 152 | GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 153 | run: | 154 | RELEASE_TAG="${{ steps.release-notes.outputs.tag }}" 155 | 156 | # Update release notes 157 | gh release edit "$RELEASE_TAG" --notes-file release-notes.md 158 | 159 | echo "✅ Updated release notes for $RELEASE_TAG" 160 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/httproute_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test httproute 2 | templates: 3 | - httproute.yaml 4 | tests: 5 | - it: should not create HTTPRoute by default 6 | asserts: 7 | - hasDocuments: 8 | count: 0 9 | 10 | - it: should create HTTPRoute when enabled 11 | set: 12 | httpRoute.enabled: true 13 | httpRoute.parentRefs: 14 | - name: my-gateway 15 | namespace: gateway-system 16 | httpRoute.rules: 17 | - matches: 18 | - path: 19 | type: PathPrefix 20 | value: / 21 | asserts: 22 | - hasDocuments: 23 | count: 1 24 | - isKind: 25 | of: HTTPRoute 26 | - isAPIVersion: 27 | of: gateway.networking.k8s.io/v1 28 | - equal: 29 | path: metadata.name 30 | value: RELEASE-NAME-jellyfin 31 | 32 | - it: should add annotations when specified 33 | set: 34 | httpRoute.enabled: true 35 | httpRoute.annotations: 36 | my-annotation: my-value 37 | httpRoute.parentRefs: 38 | - name: my-gateway 39 | httpRoute.rules: 40 | - matches: 41 | - path: 42 | type: PathPrefix 43 | value: / 44 | asserts: 45 | - equal: 46 | path: metadata.annotations.my-annotation 47 | value: my-value 48 | 49 | - it: should set parentRefs correctly 50 | set: 51 | httpRoute.enabled: true 52 | httpRoute.parentRefs: 53 | - name: my-gateway 54 | namespace: gateway-system 55 | sectionName: https 56 | httpRoute.rules: 57 | - matches: 58 | - path: 59 | type: PathPrefix 60 | value: / 61 | asserts: 62 | - equal: 63 | path: spec.parentRefs[0].name 64 | value: my-gateway 65 | - equal: 66 | path: spec.parentRefs[0].namespace 67 | value: gateway-system 68 | - equal: 69 | path: spec.parentRefs[0].sectionName 70 | value: https 71 | 72 | - it: should set hostnames when specified 73 | set: 74 | httpRoute.enabled: true 75 | httpRoute.hostnames: 76 | - jellyfin.example.com 77 | - media.example.com 78 | httpRoute.parentRefs: 79 | - name: my-gateway 80 | httpRoute.rules: 81 | - matches: 82 | - path: 83 | type: PathPrefix 84 | value: / 85 | asserts: 86 | - equal: 87 | path: spec.hostnames[0] 88 | value: jellyfin.example.com 89 | - equal: 90 | path: spec.hostnames[1] 91 | value: media.example.com 92 | 93 | - it: should create rules with path matches 94 | set: 95 | httpRoute.enabled: true 96 | httpRoute.parentRefs: 97 | - name: my-gateway 98 | httpRoute.rules: 99 | - matches: 100 | - path: 101 | type: PathPrefix 102 | value: /jellyfin 103 | asserts: 104 | - equal: 105 | path: spec.rules[0].matches[0].path.type 106 | value: PathPrefix 107 | - equal: 108 | path: spec.rules[0].matches[0].path.value 109 | value: /jellyfin 110 | 111 | - it: should set backendRefs to jellyfin service 112 | set: 113 | httpRoute.enabled: true 114 | service.port: 8096 115 | httpRoute.parentRefs: 116 | - name: my-gateway 117 | httpRoute.rules: 118 | - matches: 119 | - path: 120 | type: PathPrefix 121 | value: / 122 | asserts: 123 | - equal: 124 | path: spec.rules[0].backendRefs[0].name 125 | value: RELEASE-NAME-jellyfin 126 | - equal: 127 | path: spec.rules[0].backendRefs[0].port 128 | value: 8096 129 | 130 | - it: should support multiple rules 131 | set: 132 | httpRoute.enabled: true 133 | httpRoute.parentRefs: 134 | - name: my-gateway 135 | httpRoute.rules: 136 | - matches: 137 | - path: 138 | type: PathPrefix 139 | value: / 140 | - matches: 141 | - path: 142 | type: Exact 143 | value: /api 144 | asserts: 145 | - lengthEqual: 146 | path: spec.rules 147 | count: 2 148 | - equal: 149 | path: spec.rules[0].matches[0].path.value 150 | value: / 151 | - equal: 152 | path: spec.rules[1].matches[0].path.value 153 | value: /api 154 | 155 | - it: should support multiple matches in single rule 156 | set: 157 | httpRoute.enabled: true 158 | httpRoute.parentRefs: 159 | - name: my-gateway 160 | httpRoute.rules: 161 | - matches: 162 | - path: 163 | type: PathPrefix 164 | value: /api 165 | - path: 166 | type: PathPrefix 167 | value: /web 168 | asserts: 169 | - lengthEqual: 170 | path: spec.rules[0].matches 171 | count: 2 172 | - equal: 173 | path: spec.rules[0].matches[0].path.value 174 | value: /api 175 | - equal: 176 | path: spec.rules[0].matches[1].path.value 177 | value: /web 178 | 179 | - it: should use custom service port 180 | set: 181 | httpRoute.enabled: true 182 | service.port: 9000 183 | httpRoute.parentRefs: 184 | - name: my-gateway 185 | httpRoute.rules: 186 | - matches: 187 | - path: 188 | type: PathPrefix 189 | value: / 190 | asserts: 191 | - equal: 192 | path: spec.rules[0].backendRefs[0].port 193 | value: 9000 194 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "jellyfin.fullname" . }} 5 | {{- with .Values.deploymentAnnotations }} 6 | annotations: 7 | {{- toYaml . | nindent 4 }} 8 | {{- end }} 9 | labels: 10 | {{- include "jellyfin.labels" . | nindent 4 }} 11 | spec: 12 | {{- with .Values.deploymentStrategy }} 13 | strategy: 14 | {{- toYaml . | trim | nindent 4 }} 15 | {{- end }} 16 | replicas: {{ .Values.replicaCount }} 17 | {{- with .Values.revisionHistoryLimit }} 18 | revisionHistoryLimit: {{ . }} 19 | {{- end }} 20 | selector: 21 | matchLabels: 22 | {{- include "jellyfin.selectorLabels" . | nindent 6 }} 23 | template: 24 | metadata: 25 | {{- with .Values.podAnnotations }} 26 | annotations: 27 | {{- toYaml . | nindent 8 }} 28 | {{- end }} 29 | labels: 30 | {{- include "jellyfin.labels" . | nindent 8 }} 31 | {{- with .Values.podLabels }} 32 | {{- toYaml . | nindent 8 }} 33 | {{- end }} 34 | spec: 35 | {{- with .Values.imagePullSecrets }} 36 | imagePullSecrets: 37 | {{- toYaml . | nindent 8 }} 38 | {{- end }} 39 | serviceAccountName: {{ include "jellyfin.serviceAccountName" . }} 40 | securityContext: 41 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 42 | {{- if or .Values.jellyfin.enableDLNA .Values.podPrivileges.hostNetwork }} 43 | hostNetwork: true 44 | {{- end }} 45 | {{- if .Values.podPrivileges.hostIPC }} 46 | hostIPC: true 47 | {{- end }} 48 | {{- if .Values.podPrivileges.hostPID }} 49 | hostPID: true 50 | {{- end }} 51 | containers: 52 | - name: {{ .Chart.Name }} 53 | securityContext: 54 | {{- toYaml .Values.securityContext | nindent 12 }} 55 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 56 | imagePullPolicy: {{ .Values.image.pullPolicy }} 57 | {{- with .Values.jellyfin.command }} 58 | command: 59 | {{- toYaml . | nindent 12 }} 60 | {{- end }} 61 | {{- with .Values.jellyfin.args }} 62 | args: 63 | {{- toYaml . | nindent 12 }} 64 | {{- end }} 65 | {{- with .Values.jellyfin.envFrom }} 66 | envFrom: 67 | {{- toYaml . | nindent 12 }} 68 | {{- end }} 69 | {{- with .Values.jellyfin.env }} 70 | env: 71 | {{- toYaml . | nindent 12 }} 72 | {{- end }} 73 | {{- if .Values.metrics.enabled }} 74 | lifecycle: 75 | postStart: 76 | exec: 77 | command: 78 | - /bin/sh 79 | - -c 80 | - | 81 | echo "Waiting for system.xml to enable metrics..." 82 | timeout=300 # 5 minutes timeout 83 | interval=5 # Check every 5 seconds 84 | elapsed=0 85 | while [ ! -f /config/config/system.xml ]; do 86 | if [ $elapsed -ge $timeout ]; then 87 | echo "ERROR: Timed out waiting for /config/config/system.xml" >&2 88 | exit 1 89 | fi 90 | sleep $interval 91 | elapsed=$((elapsed + interval)) 92 | done 93 | echo "Found /config/config/system.xml, enabling metrics..." 94 | # Check if metrics are already enabled before running sed 95 | if ! grep -q 'true' /config/config/system.xml; then 96 | sed 's,false,true,' -i /config/config/system.xml 97 | if [ $? -eq 0 ]; then 98 | echo "Metrics enabled successfully." 99 | else 100 | echo "ERROR: Failed to enable metrics in /config/config/system.xml" >&2 101 | exit 1 102 | fi 103 | else 104 | echo "Metrics already enabled." 105 | fi 106 | exit 0 107 | {{- end }} 108 | ports: 109 | - name: http 110 | containerPort: 8096 111 | protocol: TCP 112 | {{- if .Values.jellyfin.enableDLNA }} 113 | - name: dlna 114 | containerPort: 1900 115 | hostPort: 1900 116 | protocol: UDP 117 | {{- end }} 118 | {{- with .Values.startupProbe }} 119 | startupProbe: 120 | {{- toYaml . | nindent 12 }} 121 | {{- end }} 122 | livenessProbe: 123 | {{- toYaml .Values.livenessProbe | nindent 12 }} 124 | readinessProbe: 125 | {{- toYaml .Values.readinessProbe | nindent 12 }} 126 | resources: 127 | {{- toYaml .Values.resources | nindent 12 }} 128 | volumeMounts: 129 | - mountPath: /config 130 | name: config 131 | - mountPath: /media 132 | name: media 133 | - mountPath: /cache 134 | name: cache 135 | {{- with .Values.volumeMounts }} 136 | {{- toYaml . | nindent 12 }} 137 | {{- end }} 138 | {{- with .Values.extraContainers }} 139 | {{- toYaml . | nindent 8 }} 140 | {{- end }} 141 | {{- if or .Values.initContainers .Values.extraInitContainers }} 142 | # Support both initContainers (deprecated) and extraInitContainers for backward compatibility. 143 | # TODO: Remove initContainers support after 2030 144 | initContainers: 145 | {{- if .Values.initContainers }} 146 | {{- toYaml .Values.initContainers | nindent 6 }} 147 | {{- end }} 148 | {{- if .Values.extraInitContainers }} 149 | {{- toYaml .Values.extraInitContainers | nindent 6 }} 150 | {{- end }} 151 | {{- end }} 152 | volumes: 153 | - name: config 154 | {{- if .Values.persistence.config.enabled }} 155 | persistentVolumeClaim: 156 | claimName: {{ if .Values.persistence.config.existingClaim }}{{ .Values.persistence.config.existingClaim }}{{- else }}{{ template "jellyfin.fullname" . }}-config{{- end }} 157 | {{- else }} 158 | emptyDir: {} 159 | {{- end }} 160 | - name: media 161 | {{- if .Values.persistence.media.enabled }} 162 | {{- if eq .Values.persistence.media.type "pvc" }} 163 | persistentVolumeClaim: 164 | claimName: {{ if .Values.persistence.media.existingClaim }}{{ .Values.persistence.media.existingClaim }}{{- else }}{{ template "jellyfin.fullname" . }}-media{{- end }} 165 | {{- else if eq .Values.persistence.media.type "hostPath" }} 166 | hostPath: 167 | path: {{ .Values.persistence.media.hostPath }} 168 | type: Directory 169 | {{- else }} 170 | emptyDir: {} 171 | {{- end }} 172 | {{- else }} 173 | emptyDir: {} 174 | {{- end }} 175 | - name: cache 176 | {{- if .Values.persistence.cache.enabled }} 177 | {{- if eq .Values.persistence.cache.type "pvc" }} 178 | persistentVolumeClaim: 179 | claimName: {{ if .Values.persistence.cache.existingClaim }}{{ .Values.persistence.cache.existingClaim }}{{- else }}{{ template "jellyfin.fullname" . }}-cache{{- end }} 180 | {{- else if eq .Values.persistence.cache.type "hostPath" }} 181 | hostPath: 182 | path: {{ .Values.persistence.cache.hostPath }} 183 | type: Directory 184 | {{- else }} 185 | emptyDir: {} 186 | {{- end }} 187 | {{- else }} 188 | emptyDir: {} 189 | {{- end }} 190 | {{- with .Values.volumes }} 191 | {{- toYaml . | nindent 8 }} 192 | {{- end }} 193 | {{- with .Values.nodeSelector }} 194 | nodeSelector: 195 | {{- toYaml . | nindent 8 }} 196 | {{- end }} 197 | {{- with .Values.affinity }} 198 | affinity: 199 | {{- toYaml . | nindent 8 }} 200 | {{- end }} 201 | {{- with .Values.tolerations }} 202 | tolerations: 203 | {{- toYaml . | nindent 8 }} 204 | {{- end }} 205 | {{- with .Values.runtimeClassName }} 206 | runtimeClassName: {{ . | quote }} 207 | {{- end }} 208 | {{- with .Values.priorityClassName }} 209 | priorityClassName: {{ . | quote }} 210 | {{- end }} 211 | {{- with .Values.dnsConfig }} 212 | dnsConfig: 213 | {{- toYaml . | nindent 8}} 214 | {{- end }} 215 | {{- with .Values.dnsPolicy }} 216 | dnsPolicy: {{ . | quote }} 217 | {{- end }} 218 | -------------------------------------------------------------------------------- /charts/jellyfin/tests/networkpolicy_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test networkpolicy 2 | templates: 3 | - networkpolicy.yaml 4 | tests: 5 | - it: should not create NetworkPolicy by default 6 | asserts: 7 | - hasDocuments: 8 | count: 0 9 | 10 | - it: should create NetworkPolicy when enabled 11 | set: 12 | networkPolicy.enabled: true 13 | asserts: 14 | - hasDocuments: 15 | count: 1 16 | - isKind: 17 | of: NetworkPolicy 18 | - isAPIVersion: 19 | of: networking.k8s.io/v1 20 | - equal: 21 | path: metadata.name 22 | value: RELEASE-NAME-jellyfin 23 | 24 | - it: should fail when NetworkPolicy enabled with DLNA 25 | set: 26 | networkPolicy.enabled: true 27 | jellyfin.enableDLNA: true 28 | asserts: 29 | - failedTemplate: 30 | errorMessage: "NetworkPolicy cannot be enabled when hostNetwork is enabled" 31 | 32 | - it: should fail when NetworkPolicy enabled with hostNetwork 33 | set: 34 | networkPolicy.enabled: true 35 | podPrivileges.hostNetwork: true 36 | asserts: 37 | - failedTemplate: 38 | errorMessage: "NetworkPolicy cannot be enabled when hostNetwork is enabled" 39 | 40 | - it: should have both Ingress and Egress policy types by default 41 | set: 42 | networkPolicy.enabled: true 43 | asserts: 44 | - contains: 45 | path: spec.policyTypes 46 | content: Ingress 47 | - contains: 48 | path: spec.policyTypes 49 | content: Egress 50 | 51 | - it: should allow external ingress by default 52 | set: 53 | networkPolicy.enabled: true 54 | asserts: 55 | - isNotNull: 56 | path: spec.ingress 57 | # When allowExternal is true, there should be no 'from' in first ingress rule 58 | - isNull: 59 | path: spec.ingress[0].from 60 | 61 | - it: should restrict ingress when allowExternal is false 62 | set: 63 | networkPolicy.enabled: true 64 | networkPolicy.ingress.allowExternal: false 65 | networkPolicy.ingress.podSelector: 66 | matchLabels: 67 | app: test 68 | asserts: 69 | - isNotNull: 70 | path: spec.ingress[0].from 71 | - equal: 72 | path: spec.ingress[0].from[0].podSelector.matchLabels.app 73 | value: test 74 | 75 | - it: should add namespace selector for ingress when specified 76 | set: 77 | networkPolicy.enabled: true 78 | networkPolicy.ingress.allowExternal: false 79 | networkPolicy.ingress.namespaceSelector: 80 | matchLabels: 81 | name: frontend 82 | asserts: 83 | - equal: 84 | path: spec.ingress[0].from[0].namespaceSelector.matchLabels.name 85 | value: frontend 86 | 87 | - it: should create Prometheus ingress rule when metrics enabled 88 | set: 89 | networkPolicy.enabled: true 90 | metrics.enabled: true 91 | metrics.serviceMonitor.enabled: true 92 | metrics.serviceMonitor.port: 8096 93 | asserts: 94 | - contains: 95 | path: spec.ingress 96 | content: 97 | ports: 98 | - protocol: TCP 99 | port: 8096 100 | from: 101 | - podSelector: 102 | app.kubernetes.io/name: prometheus 103 | 104 | - it: should add Prometheus namespace selector when specified 105 | set: 106 | networkPolicy.enabled: true 107 | metrics.enabled: true 108 | metrics.serviceMonitor.enabled: true 109 | networkPolicy.metrics.namespace: monitoring 110 | asserts: 111 | - isNotNull: 112 | path: spec.ingress[1].from[0].namespaceSelector 113 | - equal: 114 | path: spec.ingress[1].from[0].namespaceSelector.matchLabels.kubernetes.io/metadata.name 115 | value: monitoring 116 | 117 | - it: should add custom ingress rules 118 | set: 119 | networkPolicy.enabled: true 120 | networkPolicy.ingress.customRules: 121 | - from: 122 | - namespaceSelector: 123 | matchLabels: 124 | name: custom 125 | ports: 126 | - protocol: TCP 127 | port: 9999 128 | asserts: 129 | - contains: 130 | path: spec.ingress 131 | content: 132 | from: 133 | - namespaceSelector: 134 | matchLabels: 135 | name: custom 136 | ports: 137 | - protocol: TCP 138 | port: 9999 139 | 140 | - it: should allow DNS egress by default 141 | set: 142 | networkPolicy.enabled: true 143 | asserts: 144 | - contains: 145 | path: spec.egress 146 | content: 147 | to: 148 | - namespaceSelector: 149 | matchLabels: 150 | kubernetes.io/metadata.name: kube-system 151 | podSelector: 152 | matchLabels: 153 | k8s-app: kube-dns 154 | ports: 155 | - protocol: UDP 156 | port: 53 157 | - protocol: TCP 158 | port: 53 159 | 160 | - it: should allow custom DNS namespace 161 | set: 162 | networkPolicy.enabled: true 163 | networkPolicy.egress.dnsNamespace: custom-dns-ns 164 | asserts: 165 | - equal: 166 | path: spec.egress[0].to[0].namespaceSelector.matchLabels.kubernetes.io/metadata.name 167 | value: custom-dns-ns 168 | 169 | - it: should allow custom DNS pod selector 170 | set: 171 | networkPolicy.enabled: true 172 | networkPolicy.egress.dnsPodSelector: 173 | k8s-app: coredns 174 | asserts: 175 | - equal: 176 | path: spec.egress[0].to[0].podSelector.matchLabels.k8s-app 177 | value: coredns 178 | 179 | - it: should allow all egress by default 180 | set: 181 | networkPolicy.enabled: true 182 | asserts: 183 | - contains: 184 | path: spec.egress 185 | content: 186 | to: 187 | - ipBlock: 188 | cidr: 0.0.0.0/0 189 | 190 | - it: should not allow all egress when allowAllEgress is false 191 | set: 192 | networkPolicy.enabled: true 193 | networkPolicy.egress.allowAllEgress: false 194 | asserts: 195 | - notContains: 196 | path: spec.egress 197 | content: 198 | to: 199 | - ipBlock: 200 | cidr: 0.0.0.0/0 201 | 202 | - it: should allow HTTPS when restrictedEgress.allowMetadata is true 203 | set: 204 | networkPolicy.enabled: true 205 | networkPolicy.egress.allowAllEgress: false 206 | networkPolicy.egress.restrictedEgress.allowMetadata: true 207 | asserts: 208 | - contains: 209 | path: spec.egress 210 | content: 211 | to: 212 | - ipBlock: 213 | cidr: 0.0.0.0/0 214 | ports: 215 | - protocol: TCP 216 | port: 443 217 | 218 | - it: should allow in-cluster communication when restrictedEgress.allowInCluster is true 219 | set: 220 | networkPolicy.enabled: true 221 | networkPolicy.egress.allowAllEgress: false 222 | networkPolicy.egress.restrictedEgress.allowInCluster: true 223 | asserts: 224 | - contains: 225 | path: spec.egress 226 | content: 227 | to: 228 | - podSelector: {} 229 | 230 | - it: should add custom CIDR blocks 231 | set: 232 | networkPolicy.enabled: true 233 | networkPolicy.egress.allowAllEgress: false 234 | networkPolicy.egress.restrictedEgress.allowedCIDRs: 235 | - 10.0.0.0/8 236 | - 192.168.0.0/16 237 | asserts: 238 | - contains: 239 | path: spec.egress 240 | content: 241 | to: 242 | - ipBlock: 243 | cidr: 10.0.0.0/8 244 | - contains: 245 | path: spec.egress 246 | content: 247 | to: 248 | - ipBlock: 249 | cidr: 192.168.0.0/16 250 | 251 | - it: should add custom egress rules 252 | set: 253 | networkPolicy.enabled: true 254 | networkPolicy.egress.customRules: 255 | - to: 256 | - podSelector: 257 | matchLabels: 258 | app: database 259 | ports: 260 | - protocol: TCP 261 | port: 5432 262 | asserts: 263 | - contains: 264 | path: spec.egress 265 | content: 266 | to: 267 | - podSelector: 268 | matchLabels: 269 | app: database 270 | ports: 271 | - protocol: TCP 272 | port: 5432 273 | 274 | - it: should not create DNS egress when allowDNS is false 275 | set: 276 | networkPolicy.enabled: true 277 | networkPolicy.egress.allowDNS: false 278 | asserts: 279 | - notContains: 280 | path: spec.egress 281 | content: 282 | ports: 283 | - protocol: UDP 284 | port: 53 285 | - protocol: TCP 286 | port: 53 287 | 288 | - it: should use correct pod selector labels 289 | set: 290 | networkPolicy.enabled: true 291 | asserts: 292 | - equal: 293 | path: spec.podSelector.matchLabels.app\.kubernetes\.io/name 294 | value: jellyfin 295 | - equal: 296 | path: spec.podSelector.matchLabels.app\.kubernetes\.io/instance 297 | value: RELEASE-NAME 298 | -------------------------------------------------------------------------------- /charts/jellyfin/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | {{ template "chart.deprecationWarning" . }} 3 | 4 | {{ template "chart.badgesSection" . }} 5 | 6 | {{ template "chart.description" . }} 7 | 8 | {{ template "chart.homepageLine" . }} 9 | 10 | ## Steps to Use a Helm Chart 11 | 12 | ### 1. Add a Helm Repository 13 | 14 | Helm repositories contain collections of charts. You can add an existing repository using the following command: 15 | 16 | ```bash 17 | helm repo add jellyfin https://jellyfin.github.io/jellyfin-helm 18 | ``` 19 | 20 | ### 2. Install the Helm Chart 21 | 22 | To install a chart, use the following command: 23 | 24 | ```bash 25 | helm install my-jellyfin jellyfin/jellyfin 26 | ``` 27 | 28 | ### 3. View the Installation 29 | 30 | You can check the status of the release using: 31 | 32 | ```bash 33 | helm status my-jellyfin 34 | ``` 35 | 36 | ## Customizing the Chart 37 | 38 | Helm charts come with default values, but you can customize them by using the --set flag or by providing a custom values.yaml file. 39 | 40 | ### 1. Using --set to Override Values 41 | ```bash 42 | helm install my-jellyfin jellyfin/jellyfin --set key1=value1,key2=value2 43 | ``` 44 | 45 | ### 2. Using a values.yaml File 46 | You can create a custom values.yaml file and pass it to the install command: 47 | 48 | ```bash 49 | helm install my-jellyfin jellyfin/jellyfin -f values.yaml 50 | ``` 51 | 52 | {{ template "chart.maintainersSection" . }} 53 | 54 | {{ template "chart.sourcesSection" . }} 55 | 56 | {{ template "chart.requirementsSection" . }} 57 | 58 | {{ template "chart.valuesSection" . }} 59 | 60 | {{ template "helm-docs.versionFooter" . }} 61 | 62 | ## Gateway API HTTPRoute 63 | 64 | This chart supports the Kubernetes Gateway API HTTPRoute resource as a modern alternative to Ingress. 65 | 66 | To use HTTPRoute, you need to have Gateway API CRDs installed in your cluster and a Gateway resource configured. 67 | 68 | Example configuration: 69 | 70 | ```yaml 71 | httpRoute: 72 | enabled: true 73 | annotations: {} 74 | parentRefs: 75 | - name: my-gateway 76 | namespace: gateway-system 77 | sectionName: https 78 | hostnames: 79 | - jellyfin.example.com 80 | rules: 81 | - matches: 82 | - path: 83 | type: PathPrefix 84 | value: / 85 | ``` 86 | 87 | For more information about Gateway API, see: 88 | 89 | ## Hardware acceleration 90 | 91 | Out of the box the pod does not have the necessary permissions to enable hardware acceleration (HWA) in Jellyfin. 92 | Adding the following Helm values should make it enable you to use hardware acceleration features. 93 | Some settings may need to be tweaked depending on the type of device (Intel/AMD/NVIDIA/...) and your container runtime. 94 | 95 | Please refer to the Jellyfin upstream documentation for more information about hardware acceleration: 96 | 97 | ```yaml 98 | securityContext: 99 | capabilities: 100 | add: 101 | - "SYS_ADMIN" 102 | drop: 103 | - "ALL" 104 | privileged: false 105 | 106 | extraVolumes: 107 | - name: hwa 108 | hostPath: 109 | path: /dev/dri 110 | 111 | extraVolumeMounts: 112 | - name: hwa 113 | mountPath: /dev/dri 114 | ``` 115 | 116 | ## Network Security 117 | 118 | Jellyfin chart supports Kubernetes NetworkPolicy for network isolation and security hardening. NetworkPolicy allows you to control which pods can access Jellyfin (ingress) and what external connections Jellyfin can make (egress). 119 | 120 | ### Requirements 121 | 122 | - **CNI Plugin**: NetworkPolicy requires a Container Network Interface (CNI) plugin that supports NetworkPolicies, such as: 123 | - Calico 124 | - Cilium 125 | - Weave Net 126 | - Canal 127 | 128 | Check with your cluster administrator if NetworkPolicy is supported in your cluster. 129 | 130 | - **DLNA Incompatibility**: NetworkPolicy cannot be enabled when `enableDLNA: true` or `podPrivileges.hostNetwork: true` is set, as pods using `hostNetwork` bypass NetworkPolicy rules. The chart will fail deployment with a clear error message if both are enabled. 131 | 132 | ### Basic Usage 133 | 134 | By default, NetworkPolicy is disabled to maintain backward compatibility. To enable basic network isolation: 135 | 136 | ```yaml 137 | networkPolicy: 138 | enabled: true 139 | ``` 140 | 141 | This will create a NetworkPolicy with the following defaults: 142 | - **Ingress**: Allow connections from any pod in any namespace 143 | - **Egress**: Allow DNS resolution and all internet access (required for metadata) 144 | 145 | ### Production Configuration - Restrict to Ingress Controller 146 | 147 | For production environments, you typically want to restrict access to only allow traffic through the Ingress controller: 148 | 149 | ```yaml 150 | networkPolicy: 151 | enabled: true 152 | ingress: 153 | allowExternal: false 154 | namespaceSelector: 155 | matchLabels: 156 | name: ingress-nginx 157 | podSelector: 158 | matchLabels: 159 | app.kubernetes.io/name: ingress-nginx 160 | 161 | ingress: 162 | enabled: true 163 | className: nginx 164 | hosts: 165 | - host: jellyfin.example.com 166 | paths: 167 | - path: / 168 | pathType: Prefix 169 | ``` 170 | 171 | ### High Security Configuration - Restricted Egress 172 | 173 | For security-conscious deployments that need to limit outbound connections: 174 | 175 | ```yaml 176 | networkPolicy: 177 | enabled: true 178 | ingress: 179 | allowExternal: false 180 | podSelector: 181 | matchLabels: 182 | jellyfin-client: "true" # Only pods with this label can access 183 | egress: 184 | allowDNS: true # Always needed 185 | allowAllEgress: false # Block unrestricted internet 186 | restrictedEgress: 187 | allowMetadata: true # Allow HTTPS/443 for metadata providers (TMDB, etc.) 188 | allowInCluster: false # Block pod-to-pod communication 189 | ``` 190 | 191 | **Note**: With this configuration, Jellyfin can only: 192 | - Resolve DNS queries 193 | - Connect to HTTPS (port 443) endpoints for metadata providers 194 | - Cannot connect to other pods in the cluster 195 | - Cannot access non-HTTPS services 196 | 197 | ### Monitoring Integration 198 | 199 | If you're using Prometheus for monitoring, the chart automatically allows ingress from Prometheus pods when metrics are enabled: 200 | 201 | ```yaml 202 | networkPolicy: 203 | enabled: true 204 | metrics: 205 | enabled: true 206 | serviceMonitor: 207 | enabled: true 208 | ``` 209 | 210 | By default, the chart allows ingress from pods with label `app.kubernetes.io/name: prometheus`. If your Prometheus uses different labels, customize the selector: 211 | 212 | ```yaml 213 | networkPolicy: 214 | enabled: true 215 | metrics: 216 | namespace: monitoring # If Prometheus is in a different namespace 217 | podSelector: 218 | app: my-prometheus 219 | ``` 220 | 221 | ### Advanced Configuration 222 | 223 | #### Multiple Namespaces Access 224 | 225 | Allow access from multiple namespaces using custom rules: 226 | 227 | ```yaml 228 | networkPolicy: 229 | enabled: true 230 | ingress: 231 | allowExternal: false 232 | customRules: 233 | # Frontend namespace 234 | - from: 235 | - namespaceSelector: 236 | matchLabels: 237 | name: frontend 238 | podSelector: 239 | matchLabels: 240 | access-jellyfin: "true" 241 | ports: 242 | - protocol: TCP 243 | port: 8096 244 | 245 | # Admin tools namespace 246 | - from: 247 | - namespaceSelector: 248 | matchLabels: 249 | name: admin-tools 250 | ports: 251 | - protocol: TCP 252 | port: 8096 253 | ``` 254 | 255 | #### Custom Egress Rules 256 | 257 | Allow connections to specific external services: 258 | 259 | ```yaml 260 | networkPolicy: 261 | enabled: true 262 | egress: 263 | allowAllEgress: false 264 | restrictedEgress: 265 | allowMetadata: true 266 | allowedCIDRs: 267 | - 10.0.0.0/8 # Internal network 268 | - 192.168.0.0/16 # Another internal network 269 | customRules: 270 | # Allow connection to external database 271 | - to: 272 | - ipBlock: 273 | cidr: 203.0.113.0/24 274 | ports: 275 | - protocol: TCP 276 | port: 5432 277 | ``` 278 | 279 | ### Security Considerations 280 | 281 | 1. **Metadata Providers**: Jellyfin requires internet access to download metadata (movie posters, descriptions, etc.) from: 282 | - TheMovieDB (api.themoviedb.org) 283 | - TheTVDB (api.thetvdb.com) 284 | - OpenSubtitles (api.opensubtitles.com) 285 | - Fanart.tv (fanart.tv) 286 | 287 | If you use `restrictedEgress.allowMetadata: true`, these will work as they all use HTTPS (port 443). 288 | 289 | 2. **DNS Access**: DNS resolution is critical for Jellyfin operation. The chart prevents accidental DNS blocking by defaulting `allowDNS: true`. 290 | 291 | 3. **Local Metadata**: If you want to completely block internet access, you can use local metadata (NFO files and local images). This requires manual setup and is not the default Jellyfin behavior. 292 | 293 | 4. **Testing**: Always test NetworkPolicy changes in a development environment first. Misconfigured policies can block legitimate traffic. 294 | 295 | ### Troubleshooting 296 | 297 | **Jellyfin can't download metadata/images:** 298 | - Check that `egress.allowAllEgress: true` or `restrictedEgress.allowMetadata: true` is set 299 | - Verify DNS egress is allowed: `egress.allowDNS: true` 300 | 301 | **Can't access Jellyfin web interface:** 302 | - Verify ingress rules allow traffic from your access point (Ingress controller, LoadBalancer, etc.) 303 | - Check NOTES.txt after deployment for detailed NetworkPolicy status 304 | 305 | **Prometheus can't scrape metrics:** 306 | - Ensure `metrics.enabled: true` and `metrics.serviceMonitor.enabled: true` 307 | - Verify `networkPolicy.metrics.podSelector` matches your Prometheus labels 308 | - Set `networkPolicy.metrics.namespace` if Prometheus is in a different namespace 309 | 310 | **Deployment fails with "NetworkPolicy cannot be enabled...":** 311 | - You have both `networkPolicy.enabled: true` and `hostNetwork: true` (or `enableDLNA: true`) 312 | - NetworkPolicy doesn't work with host networking 313 | - Either disable NetworkPolicy or disable host networking 314 | 315 | For more configuration options, see the full values documentation in [values.yaml](values.yaml). 316 | ## Troubleshooting 317 | 318 | ### inotify Instance Limit Reached 319 | 320 | **Problem:** Jellyfin crashes with error: 321 | ``` 322 | System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached 323 | ``` 324 | 325 | **Root cause:** The Linux kernel has a limit on inotify instances (file system watchers) per user. Jellyfin uses inotify to monitor media libraries for changes. 326 | 327 | **Proper solution (recommended):** 328 | 329 | Increase the inotify limit on the Kubernetes nodes: 330 | 331 | ```bash 332 | # Temporary (until reboot) 333 | sysctl -w fs.inotify.max_user_instances=512 334 | 335 | # Permanent 336 | echo "fs.inotify.max_user_instances=512" >> /etc/sysctl.conf 337 | sysctl -p 338 | ``` 339 | 340 | Recommended values: 341 | - `fs.inotify.max_user_instances`: 512 or higher 342 | - `fs.inotify.max_user_watches`: 524288 or higher (if you have large media libraries) 343 | 344 | **Workaround (if you cannot modify host settings):** 345 | 346 | If you're running on a managed Kubernetes cluster where you cannot modify node-level settings, you can force Jellyfin to use polling instead of inotify. **Note: This is less efficient and may increase CPU usage and delay change detection.** 347 | 348 | ```yaml 349 | jellyfin: 350 | env: 351 | - name: DOTNET_USE_POLLING_FILE_WATCHER 352 | value: "1" 353 | ``` 354 | 355 | This workaround disables inotify file watching in favor of periodic polling, which doesn't require inotify instances but is less efficient. 356 | 357 | ## IPv6 Configuration 358 | 359 | This chart supports IPv6 and dual-stack networking configurations out of the box. Health probes use httpGet by default for compatibility with both IPv4 and IPv6. 360 | 361 | ### IPv6-only Configuration 362 | 363 | For IPv6-only clusters: 364 | 365 | ```yaml 366 | service: 367 | ipFamilyPolicy: SingleStack 368 | ipFamilies: 369 | - IPv6 370 | ``` 371 | 372 | ### Dual-stack Configuration 373 | 374 | For dual-stack clusters (both IPv4 and IPv6): 375 | 376 | ```yaml 377 | service: 378 | ipFamilyPolicy: PreferDualStack # or RequireDualStack 379 | ipFamilies: 380 | - IPv4 381 | - IPv6 # First family in the list is the primary 382 | ``` 383 | 384 | For more information about Kubernetes dual-stack networking, see: 385 | -------------------------------------------------------------------------------- /charts/jellyfin/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | {{- if .Values.initContainers }} 2 | ############################################################################# 3 | ## ⚠️ WARNING ⚠️ ## 4 | ## ## 5 | ## The 'initContainers' parameter is DEPRECATED! ## 6 | ## Please migrate to 'extraInitContainers' instead. ## 7 | ## ## 8 | ## Support for 'initContainers' will be removed after 2030. ## 9 | ## ## 10 | ## Migration guide: ## 11 | ## Old: initContainers: [...] ## 12 | ## New: extraInitContainers: [...] ## 13 | ## ## 14 | ############################################################################# 15 | 16 | {{- end }} 17 | Thank you for installing {{ .Chart.Name }}! 18 | 19 | Your release is named {{ .Release.Name }}. 20 | 21 | To learn more about the release, try: 22 | 23 | $ helm status {{ .Release.Name }} --namespace {{ .Release.Namespace }} 24 | $ helm get all {{ .Release.Name }} --namespace {{ .Release.Namespace }} 25 | 26 | {{- if .Values.ingress.enabled }} 27 | {{- range $host := .Values.ingress.hosts }} 28 | {{- range .paths }} 29 | 30 | Jellyfin is available at: 31 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 32 | {{- end }} 33 | {{- end }} 34 | {{- else if contains "NodePort" .Values.service.type }} 35 | 36 | Get the Jellyfin URL by running: 37 | 38 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "jellyfin.fullname" . }}) 39 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 40 | echo "Jellyfin URL: http://$NODE_IP:$NODE_PORT" 41 | 42 | {{- else if contains "LoadBalancer" .Values.service.type }} 43 | 44 | Get the Jellyfin URL by running: 45 | 46 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 47 | You can watch the status by running: 48 | kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "jellyfin.fullname" . }} 49 | 50 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "jellyfin.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 51 | echo "Jellyfin URL: http://$SERVICE_IP:{{ .Values.service.port }}" 52 | 53 | {{- else if contains "ClusterIP" .Values.service.type }} 54 | 55 | Get the Jellyfin URL by running: 56 | 57 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "jellyfin.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 58 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 59 | echo "Jellyfin URL: http://127.0.0.1:8080" 60 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 61 | 62 | {{- end }} 63 | 64 | {{- if .Values.persistence.config.enabled }} 65 | 66 | Configuration is persisted to: 67 | {{- if .Values.persistence.config.existingClaim }} 68 | Existing PVC: {{ .Values.persistence.config.existingClaim }} 69 | {{- else }} 70 | PVC: {{ include "jellyfin.fullname" . }}-config 71 | {{- end }} 72 | {{- else }} 73 | 74 | WARNING: Configuration persistence is disabled! 75 | Data will be lost when the pod is restarted. 76 | Enable persistence.config.enabled to persist your configuration. 77 | {{- end }} 78 | 79 | {{- if .Values.persistence.media.enabled }} 80 | 81 | Media is {{ if eq .Values.persistence.media.type "hostPath" }}mounted from host path{{ else if eq .Values.persistence.media.type "pvc" }}persisted to PVC{{ else }}stored in emptyDir{{ end }}: 82 | {{- if eq .Values.persistence.media.type "hostPath" }} 83 | Host path: {{ .Values.persistence.media.hostPath }} 84 | {{- else if eq .Values.persistence.media.type "pvc" }} 85 | {{- if .Values.persistence.media.existingClaim }} 86 | Existing PVC: {{ .Values.persistence.media.existingClaim }} 87 | {{- else }} 88 | PVC: {{ include "jellyfin.fullname" . }}-media 89 | {{- end }} 90 | {{- end }} 91 | {{- else }} 92 | 93 | WARNING: Media persistence is disabled! 94 | Your media files will be lost when the pod is restarted. 95 | Enable persistence.media.enabled to persist your media. 96 | {{- end }} 97 | 98 | For more information and documentation: 99 | Repository: https://github.com/jellyfin/jellyfin-helm 100 | Jellyfin Docs: https://jellyfin.org/docs/ 101 | $ helm status {{ .Release.Name }} 102 | $ helm get all {{ .Release.Name }} 103 | 104 | {{- if .Values.httpRoute.enabled }} 105 | 106 | HTTPRoute (Gateway API) is ENABLED: 107 | {{- if .Values.httpRoute.parentRefs }} 108 | Gateway References: 109 | {{- range .Values.httpRoute.parentRefs }} 110 | - {{ .name }}{{ if .namespace }} (namespace: {{ .namespace }}){{ end }}{{ if .sectionName }} [{{ .sectionName }}]{{ end }} 111 | {{- end }} 112 | {{- else }} 113 | ⚠ WARNING: No parentRefs defined - HTTPRoute will not attach to any Gateway! 114 | {{- end }} 115 | {{- if .Values.httpRoute.hostnames }} 116 | Hostnames: 117 | {{- range .Values.httpRoute.hostnames }} 118 | - {{ . }} 119 | {{- end }} 120 | {{- end }} 121 | Rules: {{ len .Values.httpRoute.rules }} route(s) configured 122 | Backend: {{ include "jellyfin.fullname" . }}:{{ .Values.service.port }} 123 | 124 | Access Jellyfin via Gateway API at: 125 | {{- if .Values.httpRoute.hostnames }} 126 | {{- range .Values.httpRoute.hostnames }} 127 | https://{{ . }}{{ (index $.Values.httpRoute.rules 0).matches 0 .path.value }} 128 | {{- end }} 129 | {{- else }} 130 | (Configure httpRoute.hostnames for external access) 131 | {{- end }} 132 | 133 | Note: Ensure your Gateway API CRDs are installed and Gateway is configured. 134 | {{- else if .Values.ingress.enabled }} 135 | {{- if .Values.ingress.enabled }} 136 | 137 | Jellyfin is available via Ingress at: 138 | {{- range .Values.ingress.hosts }} 139 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}{{ (index .paths 0).path }} 140 | {{- end }} 141 | {{- else }} 142 | 143 | To access Jellyfin, you can use port-forwarding: 144 | 145 | $ kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "jellyfin.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} 146 | 147 | Then visit http://localhost:{{ .Values.service.port }} in your browser. 148 | {{- end }} 149 | 150 | {{- if .Values.networkPolicy.enabled }} 151 | 152 | Network Policy is ENABLED: 153 | {{- if or .Values.jellyfin.enableDLNA .Values.podPrivileges.hostNetwork }} 154 | WARNING: NetworkPolicy is incompatible with hostNetwork (DLNA mode). 155 | The deployment will FAIL. Please disable networkPolicy.enabled or hostNetwork. 156 | {{- else }} 157 | 158 | Ingress Policy: 159 | {{- if .Values.networkPolicy.ingress.allowExternal }} 160 | - ✓ Allowing connections from ANY pod in ANY namespace 161 | {{- else }} 162 | {{- if or .Values.networkPolicy.ingress.podSelector .Values.networkPolicy.ingress.namespaceSelector }} 163 | - ✓ Allowing connections ONLY from pods matching: 164 | {{- if .Values.networkPolicy.ingress.podSelector }} 165 | Pod Labels: {{ .Values.networkPolicy.ingress.podSelector | toYaml | nindent 8 }} 166 | {{- end }} 167 | {{- if .Values.networkPolicy.ingress.namespaceSelector }} 168 | Namespace Labels: {{ .Values.networkPolicy.ingress.namespaceSelector | toYaml | nindent 8 }} 169 | {{- end }} 170 | {{- else }} 171 | - ⚠ WARNING: allowExternal=false but no selectors defined - NO pods can connect! 172 | {{- end }} 173 | {{- end }} 174 | {{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} 175 | - ✓ Allowing Prometheus metrics scraping from {{ .Values.networkPolicy.metrics.podSelector | toYaml }} 176 | {{- end }} 177 | {{- if .Values.networkPolicy.ingress.customRules }} 178 | - ✓ Custom ingress rules applied ({{ len .Values.networkPolicy.ingress.customRules }} rules) 179 | {{- end }} 180 | 181 | Egress Policy: 182 | {{- if .Values.networkPolicy.egress.allowDNS }} 183 | - ✓ DNS resolution allowed ({{ .Values.networkPolicy.egress.dnsNamespace }}/{{ .Values.networkPolicy.egress.dnsPodSelector | toYaml }}) 184 | {{- else }} 185 | - ⚠ WARNING: DNS is DISABLED - Jellyfin will NOT function properly! 186 | {{- end }} 187 | {{- if .Values.networkPolicy.egress.allowAllEgress }} 188 | - ✓ All egress allowed (metadata, subtitles, images) 189 | {{- else }} 190 | {{- if .Values.networkPolicy.egress.restrictedEgress.allowMetadata }} 191 | - ✓ HTTPS/443 allowed (metadata providers) 192 | {{- end }} 193 | {{- if .Values.networkPolicy.egress.restrictedEgress.allowInCluster }} 194 | - ✓ In-cluster communication allowed 195 | {{- end }} 196 | {{- if .Values.networkPolicy.egress.restrictedEgress.allowedCIDRs }} 197 | - ✓ Custom CIDRs allowed: {{ .Values.networkPolicy.egress.restrictedEgress.allowedCIDRs | join ", " }} 198 | {{- end }} 199 | {{- if not (or .Values.networkPolicy.egress.restrictedEgress.allowMetadata .Values.networkPolicy.egress.restrictedEgress.allowInCluster .Values.networkPolicy.egress.restrictedEgress.allowedCIDRs) }} 200 | - ⚠ WARNING: No egress rules defined - Jellyfin cannot download metadata! 201 | {{- end }} 202 | {{- end }} 203 | {{- if .Values.networkPolicy.egress.customRules }} 204 | - ✓ Custom egress rules applied ({{ len .Values.networkPolicy.egress.customRules }} rules) 205 | {{- end }} 206 | 207 | Note: NetworkPolicy requires a CNI plugin with NetworkPolicy support (Calico, Cilium, etc.) 208 | {{- end }} 209 | {{- else }} 210 | 211 | Network Policy is DISABLED (default). 212 | To enable network isolation, set networkPolicy.enabled=true in your values. 213 | {{- end }} 214 | For troubleshooting common issues, see the README: 215 | - inotify instance limit errors 216 | - Hardware acceleration setup 217 | - Network configuration 218 | 219 | Useful resources: 220 | Documentation: https://jellyfin.org/docs/ 221 | Troubleshooting: See README.md in the chart repository 222 | 223 | {{- if .Values.jellyfin.envFrom }} 224 | 225 | Environment Configuration: 226 | Loading environment variables from {{ len .Values.jellyfin.envFrom }} source(s): 227 | {{- range .Values.jellyfin.envFrom }} 228 | {{- if .configMapRef }} 229 | - ConfigMap: {{ .configMapRef.name }}{{ if .configMapRef.optional }} (optional){{ end }}{{ if .prefix }} with prefix: {{ .prefix }}{{ end }} 230 | {{- else if .secretRef }} 231 | - Secret: {{ .secretRef.name }}{{ if .secretRef.optional }} (optional){{ end }}{{ if .prefix }} with prefix: {{ .prefix }}{{ end }} 232 | {{- end }} 233 | {{- end }} 234 | {{- end }} 235 | 236 | {{- if .Values.startupProbe }} 237 | {{- $startupTimeout := mul (default 10 .Values.startupProbe.periodSeconds) (default 30 .Values.startupProbe.failureThreshold) }} 238 | 239 | Startup Configuration: 240 | Startup Probe: Enabled 241 | Max Startup Time: {{ $startupTimeout }}s ({{ div $startupTimeout 60 }} minutes) 242 | 243 | The startup probe gives Jellyfin enough time to initialize, especially 244 | with large media libraries. After startup succeeds, liveness and 245 | readiness probes take over for health monitoring. 246 | 247 | {{- if gt $startupTimeout 600 }} 248 | ⚠ NOTE: Startup timeout is very long ({{ div $startupTimeout 60 }}+ minutes). 249 | Consider optimizing your media library or reducing failureThreshold. 250 | {{- end }} 251 | {{- end }} 252 | 253 | Persistence Configuration: 254 | Config: {{ if .Values.persistence.config.enabled }}{{ .Values.persistence.config.type | default "pvc" }}{{ if .Values.persistence.config.existingClaim }} (existing: {{ .Values.persistence.config.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }} 255 | Media: {{ if .Values.persistence.media.enabled }}{{ .Values.persistence.media.type | default "pvc" }}{{ if .Values.persistence.media.existingClaim }} (existing: {{ .Values.persistence.media.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }} 256 | Cache: {{ if .Values.persistence.cache.enabled }}{{ .Values.persistence.cache.type | default "pvc" }}{{ if .Values.persistence.cache.existingClaim }} (existing: {{ .Values.persistence.cache.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }} 257 | 258 | {{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "pvc") }} 259 | Cache Volume: 260 | Dedicated cache volume is enabled for improved performance. 261 | Size: {{ .Values.persistence.cache.size }} 262 | Access Mode: {{ .Values.persistence.cache.accessMode }} 263 | {{- if .Values.persistence.cache.storageClass }} 264 | Storage Class: {{ .Values.persistence.cache.storageClass }} 265 | {{- end }} 266 | 267 | Benefits: 268 | - Faster transcoding and metadata operations 269 | - Separate cache lifecycle from config/media 270 | - Can use faster storage class (SSD) for cache 271 | {{- end }} 272 | 273 | {{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "hostPath") }} 274 | ⚠ WARNING: Using hostPath for cache - ensure {{ .Values.persistence.cache.hostPath }} exists on the node 275 | {{- end }} 276 | 277 | {{- if or .Values.service.ipFamilyPolicy .Values.service.ipFamilies }} 278 | 279 | Service IP Configuration: 280 | {{- if .Values.service.ipFamilyPolicy }} 281 | IP Family Policy: {{ .Values.service.ipFamilyPolicy }} 282 | {{- end }} 283 | {{- if .Values.service.ipFamilies }} 284 | IP Families: {{ .Values.service.ipFamilies | join ", " }} 285 | {{- if eq (index .Values.service.ipFamilies 0) "IPv6" }} 286 | ⚠ NOTE: IPv6 primary - ensure your probes use httpGet instead of tcpSocket for compatibility 287 | {{- end }} 288 | {{- if has "IPv6" .Values.service.ipFamilies }} 289 | {{- if not .Values.service.ipFamilyPolicy }} 290 | ⚠ WARNING: ipFamilies includes IPv6 but ipFamilyPolicy is not set 291 | {{- end }} 292 | {{- end }} 293 | {{- end }} 294 | {{- end }} 295 | Deployment Configuration: 296 | Revision History Limit: {{ .Values.revisionHistoryLimit }} 297 | {{- $limit := int .Values.revisionHistoryLimit }} 298 | {{- if eq $limit 0 }} 299 | ⚠ WARNING: Revision history is disabled - rollback will not be possible! 300 | {{- else if eq $limit 1 }} 301 | ⚠ NOTE: Only 1 revision kept - limited rollback capability 302 | {{- else }} 303 | You can rollback to any of the last {{ .Values.revisionHistoryLimit }} revisions using: 304 | kubectl rollout undo deployment/{{ include "jellyfin.fullname" . }} --namespace {{ .Release.Namespace }} 305 | {{- end }} 306 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {{description}} 294 | Copyright (C) {{year}} {{fullname}} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /charts/jellyfin/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for Jellyfin. 2 | # This is a YAML-formatted file. 3 | 4 | # -- Number of Jellyfin replicas to start. Should be left at 1. 5 | replicaCount: 1 6 | 7 | # -- Number of old ReplicaSets to retain for rollback history. 8 | # Set to 0 to disable revision history (not recommended). 9 | # If not specified, Kubernetes defaults to 10. 10 | # See: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#revision-history-limit 11 | revisionHistoryLimit: 3 12 | 13 | # -- Image pull secrets to authenticate with private repositories. 14 | # See: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ 15 | imagePullSecrets: [] 16 | 17 | image: 18 | # -- Container image repository for Jellyfin. 19 | repository: docker.io/jellyfin/jellyfin 20 | # -- Jellyfin container image tag. Leave empty to automatically use the Chart's app version. 21 | tag: "" 22 | # -- Image pull policy (Always, IfNotPresent, or Never). 23 | pullPolicy: IfNotPresent 24 | 25 | # -- Override the default name of the chart. 26 | nameOverride: "" 27 | # -- Override the default full name of the chart. 28 | fullnameOverride: "" 29 | 30 | # -- Service account configuration. See: https://kubernetes.io/docs/concepts/security/service-accounts/ 31 | serviceAccount: 32 | # -- Specifies whether to create a service account. 33 | create: true 34 | # -- Automatically mount API credentials for the service account. 35 | automount: true 36 | # -- Annotations for the service account. 37 | annotations: {} 38 | # -- Custom name for the service account. If left empty, the name will be autogenerated. 39 | name: "" 40 | 41 | # -- Annotations to add to the pod. 42 | podAnnotations: {} 43 | # -- Additional labels to add to the pod. 44 | podLabels: {} 45 | 46 | # -- Security context for the pod. 47 | podSecurityContext: {} 48 | # fsGroup: 2000 49 | 50 | # -- Security context for the container. 51 | securityContext: {} 52 | # capabilities: 53 | # drop: 54 | # - ALL 55 | # readOnlyRootFilesystem: true 56 | # runAsNonRoot: true 57 | # runAsUser: 1000 58 | 59 | # -- Define a custom runtimeClassName for the pod. 60 | runtimeClassName: '' 61 | 62 | # -- Define a priorityClassName for the pod. 63 | priorityClassName: "" 64 | 65 | # -- Define a dnsConfig. See https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config 66 | # Use this to provide a custom DNS resolver configuration 67 | dnsConfig: {} 68 | # nameservers: 69 | # - 192.0.2.1 70 | # searches: 71 | # - ns1.svc.cluster-domain.example 72 | # - my.dns.search.suffix 73 | # options: 74 | # - name: ndots 75 | # value: "2" 76 | # - name: edns0 77 | 78 | # -- Define a dnsPolicy. See https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy 79 | dnsPolicy: "" 80 | 81 | # -- Deployment strategy configuration. See `kubectl explain deployment.spec.strategy`. 82 | deploymentStrategy: 83 | type: RollingUpdate 84 | 85 | # -- Annotations to add to the deployment. 86 | deploymentAnnotations: {} 87 | 88 | service: 89 | # # -- Custom name for the service port 90 | # name: http 91 | # -- Service type (ClusterIP, NodePort, or LoadBalancer). 92 | type: ClusterIP 93 | # -- Configure dual-stack IP family policy. See: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ 94 | # Options: SingleStack, PreferDualStack, RequireDualStack 95 | # For IPv6-only clusters, use "SingleStack" with ipFamilies: ["IPv6"] 96 | # For dual-stack, use "PreferDualStack" or "RequireDualStack" with ipFamilies: ["IPv4", "IPv6"] or ["IPv6", "IPv4"] 97 | ipFamilyPolicy: "" 98 | # -- Supported IP families (IPv4, IPv6). 99 | # Examples: 100 | # IPv4 only: ["IPv4"] 101 | # IPv6 only: ["IPv6"] 102 | # Dual-stack (IPv4 primary): ["IPv4", "IPv6"] 103 | # Dual-stack (IPv6 primary): ["IPv6", "IPv4"] 104 | # Note: When using IPv6, ensure your health checks are compatible (consider using httpGet instead of tcpSocket) 105 | ipFamilies: [] 106 | # -- Specific IP address for the LoadBalancer. 107 | loadBalancerIP: "" 108 | # -- Class of the LoadBalancer. 109 | loadBalancerClass: "" 110 | # -- Source ranges allowed to access the LoadBalancer. 111 | loadBalancerSourceRanges: [] 112 | # -- Port for the Jellyfin service. 113 | port: 8096 114 | # -- Name of the port in the service. 115 | portName: service 116 | # -- Annotations for the service. 117 | annotations: {} 118 | # -- Labels for the service. 119 | labels: {} 120 | # -- External traffic policy (Cluster or Local). 121 | # externalTrafficPolicy: Cluster 122 | # -- NodePort for the service (if applicable). 123 | # nodePort: 124 | 125 | # -- Ingress configuration. See: https://kubernetes.io/docs/concepts/services-networking/ingress/ 126 | ingress: 127 | enabled: false 128 | className: "" 129 | annotations: {} 130 | # kubernetes.io/ingress.class: nginx 131 | # kubernetes.io/tls-acme: "true" 132 | hosts: 133 | - host: chart-example.local 134 | paths: 135 | - path: / 136 | pathType: ImplementationSpecific 137 | tls: [] 138 | # - secretName: chart-example-tls 139 | # hosts: 140 | # - chart-example.local 141 | 142 | # -- HTTPRoute configuration for Gateway API. See: https://gateway-api.sigs.k8s.io/ 143 | httpRoute: 144 | enabled: false 145 | annotations: {} 146 | # -- Gateway references to attach this HTTPRoute to 147 | parentRefs: [] 148 | # - name: my-gateway 149 | # namespace: gateway-system 150 | # sectionName: https 151 | # -- Hostnames to match for this HTTPRoute 152 | hostnames: [] 153 | # - jellyfin.example.com 154 | # -- Rules for routing traffic 155 | rules: 156 | - matches: 157 | - path: 158 | type: PathPrefix 159 | value: / 160 | 161 | # -- Resource requests and limits for the Jellyfin container. 162 | resources: {} 163 | # limits: 164 | # cpu: 100m 165 | # memory: 128Mi 166 | # requests: 167 | # cpu: 100m 168 | # memory: 128Mi 169 | 170 | # -- Configure startup probe for Jellyfin. 171 | # This probe gives Jellyfin enough time to start, especially with large media libraries. 172 | # After the startup probe succeeds once, liveness and readiness probes take over. 173 | startupProbe: 174 | tcpSocket: 175 | port: http 176 | initialDelaySeconds: 0 177 | periodSeconds: 10 178 | failureThreshold: 30 179 | # With these defaults, Jellyfin has up to 5 minutes (30 * 10s) to start. 180 | # Adjust failureThreshold if you have very large media libraries or slow storage. 181 | 182 | # -- Configure liveness probe for Jellyfin. 183 | # This probe is disabled during startup (startup probe handles initial checks). 184 | # Uses httpGet for compatibility with both IPv4 and IPv6. 185 | livenessProbe: 186 | httpGet: 187 | path: /health 188 | port: http 189 | initialDelaySeconds: 10 190 | # successThreshold: 1 191 | # failureThreshold: 3 192 | # timeoutSeconds: 1 193 | # periodSeconds: 10 194 | 195 | # -- Configure readiness probe for Jellyfin. 196 | # This probe is disabled during startup (startup probe handles initial checks). 197 | # Uses httpGet for compatibility with both IPv4 and IPv6. 198 | readinessProbe: 199 | httpGet: 200 | path: /health 201 | port: http 202 | initialDelaySeconds: 10 203 | # successThreshold: 1 204 | # failureThreshold: 3 205 | # timeoutSeconds: 1 206 | # periodSeconds: 10 207 | 208 | # -- Additional volumes to mount in the Jellyfin pod. 209 | volumes: [] 210 | # - name: foo 211 | # secret: 212 | # secretName: mysecret 213 | # optional: false 214 | 215 | # -- Additional volume mounts for the Jellyfin container. 216 | volumeMounts: [] 217 | # - name: foo 218 | # mountPath: "/etc/foo" 219 | # readOnly: true 220 | 221 | # -- Node selector for pod scheduling. 222 | nodeSelector: {} 223 | 224 | # -- Tolerations for pod scheduling. 225 | tolerations: [] 226 | 227 | # -- Affinity rules for pod scheduling. 228 | affinity: {} 229 | 230 | # -- Privileged pod settings for advanced use cases 231 | podPrivileges: 232 | # -- Enable hostIPC namespace. Required for NVIDIA MPS (Multi-Process Service) GPU sharing. See: https://docs.nvidia.com/deploy/mps/index.html 233 | hostIPC: false 234 | # -- Enable hostNetwork. Allows pod to use the host's network namespace. 235 | hostNetwork: false 236 | # -- Enable hostPID namespace. Allows pod to see processes on the host. 237 | hostPID: false 238 | 239 | jellyfin: 240 | # -- Enable DLNA. Requires host network. See: https://jellyfin.org/docs/general/networking/dlna.html 241 | enableDLNA: false 242 | # -- Custom command to use as container entrypoint. 243 | command: [] 244 | # -- Additional arguments for the entrypoint command. 245 | args: [] 246 | # -- Load environment variables from ConfigMap or Secret. 247 | # See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#configure-all-key-value-pairs-in-a-configmap-as-container-environment-variables 248 | # Example: 249 | # envFrom: 250 | # - configMapRef: 251 | # name: jellyfin-config 252 | # - secretRef: 253 | # name: jellyfin-secrets 254 | envFrom: [] 255 | # -- Additional environment variables for the container. 256 | # Example: Workaround for inotify limits (see Troubleshooting section in README) 257 | # Example: 258 | # env: 259 | # - name: JELLYFIN_CACHE_DIR 260 | # value: /cache 261 | env: [] 262 | # - name: DOTNET_USE_POLLING_FILE_WATCHER 263 | # value: "1" 264 | 265 | persistence: 266 | config: 267 | # -- set to false to use emptyDir 268 | enabled: true 269 | accessMode: ReadWriteOnce 270 | size: 5Gi 271 | # -- Custom annotations to be added to the PVC 272 | annotations: {} 273 | # -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. 274 | storageClass: '' 275 | ## -- Use an existing PVC for this mount 276 | # existingClaim: '' 277 | media: 278 | # -- set to false to use emptyDir 279 | enabled: true 280 | # -- Type of volume for media storage (pvc, hostPath, emptyDir). If 'enabled' is false, 'emptyDir' is used regardless of this setting. 281 | type: pvc 282 | # -- Path on the host node for media storage, only used if type is 'hostPath'. 283 | hostPath: "" 284 | # -- PVC specific settings, only used if type is 'pvc'. 285 | accessMode: ReadWriteOnce 286 | size: 25Gi 287 | # -- Custom annotations to be added to the PVC 288 | annotations: {} 289 | # -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. 290 | storageClass: '' 291 | ## -- Use an existing PVC for this mount 292 | # existingClaim: '' 293 | cache: 294 | # -- set to false to use emptyDir 295 | enabled: false 296 | # -- Type of volume for cache storage (pvc, hostPath, emptyDir). If 'enabled' is false, 'emptyDir' is used regardless of this setting. 297 | type: pvc 298 | # -- Path on the host node for cache storage, only used if type is 'hostPath'. 299 | hostPath: "" 300 | # -- PVC specific settings, only used if type is 'pvc'. 301 | accessMode: ReadWriteOnce 302 | size: 10Gi 303 | # -- Custom annotations to be added to the PVC 304 | annotations: {} 305 | # -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. 306 | storageClass: '' 307 | ## -- Use an existing PVC for this mount 308 | # existingClaim: '' 309 | 310 | # -- Configuration for metrics collection and monitoring 311 | metrics: 312 | # -- Enable or disable metrics collection 313 | enabled: false 314 | # -- Configuration for the Prometheus ServiceMonitor 315 | serviceMonitor: 316 | # -- Enable or disable the creation of a ServiceMonitor resource 317 | enabled: false 318 | # -- Namespace where the ServiceMonitor resource should be created. Defaults to Release.Namespace 319 | namespace: '' 320 | # -- Labels to add to the ServiceMonitor resource 321 | labels: {} 322 | # -- Interval at which metrics should be scraped 323 | interval: 30s 324 | # -- Timeout for scraping metrics 325 | scrapeTimeout: 30s 326 | # -- Path to scrape for metrics 327 | path: /metrics 328 | # -- Port to scrape for metrics 329 | port: 8096 330 | # -- Scheme to use for scraping metrics (http or https) 331 | scheme: http 332 | # -- TLS configuration for scraping metrics 333 | tlsConfig: {} 334 | # -- Relabeling rules for the scraped metrics 335 | relabelings: [] 336 | # -- Relabeling rules for the metrics before ingestion 337 | metricRelabelings: [] 338 | # -- Target labels to add to the scraped metrics 339 | targetLabels: [] 340 | 341 | # -- DEPRECATED: Use extraInitContainers instead. Will be removed after 2030. 342 | # @deprecated - This parameter is deprecated, use extraInitContainers instead 343 | initContainers: [] 344 | 345 | # -- Additional init containers to run inside the pod. 346 | # Init containers run before the main application container starts. 347 | # See: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ 348 | # Example: 349 | # extraInitContainers: 350 | # - name: init-config 351 | # image: busybox:1.35 352 | # command: ['sh', '-c', 'echo "Initializing..." && sleep 5'] 353 | # volumeMounts: 354 | # - name: config 355 | # mountPath: /config 356 | extraInitContainers: [] 357 | 358 | # -- additional sidecar containers to run inside the pod. 359 | extraContainers: [] 360 | 361 | # -- Network Policy configuration for network isolation and security. 362 | # Requires a CNI plugin that supports NetworkPolicy (Calico, Cilium, Weave, etc.). 363 | # WARNING: NetworkPolicy cannot be enabled when hostNetwork is used (DLNA mode). 364 | # The chart will fail with an error if both are enabled simultaneously. 365 | networkPolicy: 366 | # -- Enable NetworkPolicy for the Jellyfin pod. 367 | # By default, this is disabled to maintain backward compatibility. 368 | # When enabled, you can control which pods can access Jellyfin (ingress) 369 | # and what external connections Jellyfin can make (egress). 370 | enabled: false 371 | 372 | # -- Policy types to enforce. Both ingress and egress policies can be enabled. 373 | # See: https://kubernetes.io/docs/concepts/services-networking/network-policies/#policy-types 374 | policyTypes: 375 | - Ingress 376 | - Egress 377 | 378 | # -- Ingress rules configuration - controls which pods/namespaces can access Jellyfin. 379 | ingress: 380 | # -- Allow external access from any namespace and any pod. 381 | # When true, any pod in the cluster can access Jellyfin (default behavior). 382 | # When false, only pods matching podSelector/namespaceSelector can access. 383 | # Set to false for production environments to restrict access. 384 | allowExternal: true 385 | 386 | # -- Custom pod selector for allowed ingress traffic. 387 | # Only used when allowExternal is false. 388 | # Allows you to specify which pods can access Jellyfin based on labels. 389 | # Example - only allow pods with specific label: 390 | # podSelector: 391 | # matchLabels: 392 | # jellyfin-client: "true" 393 | podSelector: {} 394 | 395 | # -- Namespace selector to allow cross-namespace ingress. 396 | # Only used when allowExternal is false. 397 | # Allows you to specify which namespaces can access Jellyfin. 398 | # Example - only allow from ingress-nginx namespace: 399 | # namespaceSelector: 400 | # matchLabels: 401 | # name: ingress-nginx 402 | namespaceSelector: {} 403 | 404 | # -- Additional custom ingress rules. 405 | # Allows for complex scenarios not covered by the standard template. 406 | # These rules are added as-is to the NetworkPolicy ingress section. 407 | # Example - allow from monitoring namespace: 408 | # customRules: 409 | # - from: 410 | # - namespaceSelector: 411 | # matchLabels: 412 | # name: monitoring 413 | # ports: 414 | # - protocol: TCP 415 | # port: 8096 416 | customRules: [] 417 | 418 | # -- Egress rules configuration - controls what external connections Jellyfin can make. 419 | egress: 420 | # -- Allow DNS resolution (required for Jellyfin to function). 421 | # This adds an egress rule for kube-system namespace with kube-dns pods. 422 | # DNS is required for resolving metadata provider domains and subtitle services. 423 | # It is highly recommended to keep this enabled. 424 | allowDNS: true 425 | 426 | # -- DNS namespace where DNS service is running. 427 | # Usually "kube-system" but can differ in some Kubernetes distributions. 428 | dnsNamespace: kube-system 429 | 430 | # -- DNS pod selector labels. 431 | # Default selector is for kube-dns, but CoreDNS or other DNS providers 432 | # may use different labels. Adjust if needed. 433 | # Common alternatives: 434 | # k8s-app: kube-dns (default) 435 | # k8s-app: coredns 436 | # app.kubernetes.io/name: coredns 437 | dnsPodSelector: 438 | k8s-app: kube-dns 439 | 440 | # -- Allow all egress traffic (internet access for metadata, subtitles, images). 441 | # When true, Jellyfin can connect to any external destination (0.0.0.0/0). 442 | # This is the recommended default as Jellyfin needs internet access for: 443 | # - Downloading movie/TV show metadata (TMDB, TheTVDB, OMDb) 444 | # - Fetching poster images, fanart, and other artwork 445 | # - Downloading subtitles (OpenSubtitles) 446 | # - Updating plugins 447 | # When false, you must configure restrictedEgress or customRules. 448 | allowAllEgress: true 449 | 450 | # -- Restricted egress mode for security-conscious deployments. 451 | # Only used when allowAllEgress is false. 452 | # Provides fine-grained control over outbound connections. 453 | restrictedEgress: 454 | # -- Allow HTTPS (443/TCP) for metadata providers. 455 | # Most metadata providers (TMDB, TheTVDB, OpenSubtitles, Fanart.tv) 456 | # use HTTPS, so this covers the majority of use cases. 457 | # This allows connections to any IP on port 443. 458 | allowMetadata: true 459 | 460 | # -- Allow communication within the cluster (pod-to-pod). 461 | # Useful if Jellyfin needs to connect to other services in the cluster. 462 | # This allows connections to any pod in any namespace. 463 | allowInCluster: true 464 | 465 | # -- Additional IP CIDR blocks to allow egress. 466 | # Useful for allowing specific IP ranges for metadata providers 467 | # or other external services. 468 | # Example - allow entire internet except private networks: 469 | # allowedCIDRs: 470 | # - 0.0.0.0/0 471 | # Example - allow specific metadata provider IP ranges: 472 | # allowedCIDRs: 473 | # - 13.224.0.0/14 # CloudFront (used by many CDNs) 474 | allowedCIDRs: [] 475 | 476 | # -- Additional custom egress rules. 477 | # Allows for complex scenarios not covered by the standard template. 478 | # These rules are added as-is to the NetworkPolicy egress section. 479 | # Example - allow connections to specific database: 480 | # customRules: 481 | # - to: 482 | # - podSelector: 483 | # matchLabels: 484 | # app: postgresql 485 | # ports: 486 | # - protocol: TCP 487 | # port: 5432 488 | customRules: [] 489 | 490 | # -- Prometheus metrics scraping configuration. 491 | # Automatically allows ingress from Prometheus when metrics.serviceMonitor.enabled is true. 492 | # This ensures Prometheus can scrape metrics without additional configuration. 493 | metrics: 494 | # -- Namespace where Prometheus is running. 495 | # Leave empty to use the same namespace as Jellyfin. 496 | # Set to the monitoring namespace if Prometheus is in a different namespace. 497 | # Example: "monitoring" or "prometheus" 498 | namespace: "" 499 | 500 | # -- Pod selector for Prometheus pods. 501 | # These labels must match your Prometheus deployment. 502 | # The chart will automatically add an ingress rule for pods matching these labels. 503 | # Default selector works with prometheus-operator and kube-prometheus-stack. 504 | # Adjust if your Prometheus uses different labels. 505 | podSelector: 506 | app.kubernetes.io/name: prometheus 507 | -------------------------------------------------------------------------------- /charts/jellyfin/README.md: -------------------------------------------------------------------------------- 1 | # jellyfin 2 | 3 | ![Version: 3.0.0](https://img.shields.io/badge/Version-3.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 10.11.5](https://img.shields.io/badge/AppVersion-10.11.5-informational?style=flat-square) 4 | 5 | A Helm chart for Jellyfin Media Server 6 | 7 | **Homepage:** 8 | 9 | ## Steps to Use a Helm Chart 10 | 11 | ### 1. Add a Helm Repository 12 | 13 | Helm repositories contain collections of charts. You can add an existing repository using the following command: 14 | 15 | ```bash 16 | helm repo add jellyfin https://jellyfin.github.io/jellyfin-helm 17 | ``` 18 | 19 | ### 2. Install the Helm Chart 20 | 21 | To install a chart, use the following command: 22 | 23 | ```bash 24 | helm install my-jellyfin jellyfin/jellyfin 25 | ``` 26 | 27 | ### 3. View the Installation 28 | 29 | You can check the status of the release using: 30 | 31 | ```bash 32 | helm status my-jellyfin 33 | ``` 34 | 35 | ## Customizing the Chart 36 | 37 | Helm charts come with default values, but you can customize them by using the --set flag or by providing a custom values.yaml file. 38 | 39 | ### 1. Using --set to Override Values 40 | ```bash 41 | helm install my-jellyfin jellyfin/jellyfin --set key1=value1,key2=value2 42 | ``` 43 | 44 | ### 2. Using a values.yaml File 45 | You can create a custom values.yaml file and pass it to the install command: 46 | 47 | ```bash 48 | helm install my-jellyfin jellyfin/jellyfin -f values.yaml 49 | ``` 50 | 51 | ## Values 52 | 53 | | Key | Type | Default | Description | 54 | |-----|------|---------|-------------| 55 | | affinity | object | `{}` | Affinity rules for pod scheduling. | 56 | | deploymentAnnotations | object | `{}` | Annotations to add to the deployment. | 57 | | deploymentStrategy | object | `{"type":"RollingUpdate"}` | Deployment strategy configuration. See `kubectl explain deployment.spec.strategy`. | 58 | | dnsConfig | object | `{}` | Define a dnsConfig. See https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config Use this to provide a custom DNS resolver configuration | 59 | | dnsPolicy | string | `""` | Define a dnsPolicy. See https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy | 60 | | extraContainers | list | `[]` | additional sidecar containers to run inside the pod. | 61 | | extraInitContainers | list | `[]` | Additional init containers to run inside the pod. Init containers run before the main application container starts. See: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ Example: extraInitContainers: - name: init-config image: busybox:1.35 command: ['sh', '-c', 'echo "Initializing..." && sleep 5'] volumeMounts: - name: config mountPath: /config | 62 | | fullnameOverride | string | `""` | Override the default full name of the chart. | 63 | | httpRoute | object | `{"annotations":{},"enabled":false,"hostnames":[],"parentRefs":[],"rules":[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]}` | HTTPRoute configuration for Gateway API. See: https://gateway-api.sigs.k8s.io/ | 64 | | httpRoute.hostnames | list | `[]` | Hostnames to match for this HTTPRoute | 65 | | httpRoute.parentRefs | list | `[]` | Gateway references to attach this HTTPRoute to | 66 | | httpRoute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Rules for routing traffic | 67 | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy (Always, IfNotPresent, or Never). | 68 | | image.repository | string | `"docker.io/jellyfin/jellyfin"` | Container image repository for Jellyfin. | 69 | | image.tag | string | `""` | Jellyfin container image tag. Leave empty to automatically use the Chart's app version. | 70 | | imagePullSecrets | list | `[]` | Image pull secrets to authenticate with private repositories. See: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ | 71 | | ingress | object | `{"annotations":{},"className":"","enabled":false,"hosts":[{"host":"chart-example.local","paths":[{"path":"/","pathType":"ImplementationSpecific"}]}],"tls":[]}` | Ingress configuration. See: https://kubernetes.io/docs/concepts/services-networking/ingress/ | 72 | | initContainers | list | `[]` | DEPRECATED: Use extraInitContainers instead. Will be removed after 2030. @deprecated - This parameter is deprecated, use extraInitContainers instead | 73 | | jellyfin.args | list | `[]` | Additional arguments for the entrypoint command. | 74 | | jellyfin.command | list | `[]` | Custom command to use as container entrypoint. | 75 | | jellyfin.enableDLNA | bool | `false` | Enable DLNA. Requires host network. See: https://jellyfin.org/docs/general/networking/dlna.html | 76 | | jellyfin.env | list | `[]` | Additional environment variables for the container. Example: Workaround for inotify limits (see Troubleshooting section in README) Example: env: - name: JELLYFIN_CACHE_DIR value: /cache | 77 | | jellyfin.envFrom | list | `[]` | Load environment variables from ConfigMap or Secret. See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#configure-all-key-value-pairs-in-a-configmap-as-container-environment-variables Example: envFrom: - configMapRef: name: jellyfin-config - secretRef: name: jellyfin-secrets | 78 | | livenessProbe | object | `{"httpGet":{"path":"/health","port":"http"},"initialDelaySeconds":10}` | Configure liveness probe for Jellyfin. This probe is disabled during startup (startup probe handles initial checks). Uses httpGet for compatibility with both IPv4 and IPv6. | 79 | | metrics | object | `{"enabled":false,"serviceMonitor":{"enabled":false,"interval":"30s","labels":{},"metricRelabelings":[],"namespace":"","path":"/metrics","port":8096,"relabelings":[],"scheme":"http","scrapeTimeout":"30s","targetLabels":[],"tlsConfig":{}}}` | Configuration for metrics collection and monitoring | 80 | | metrics.enabled | bool | `false` | Enable or disable metrics collection | 81 | | metrics.serviceMonitor | object | `{"enabled":false,"interval":"30s","labels":{},"metricRelabelings":[],"namespace":"","path":"/metrics","port":8096,"relabelings":[],"scheme":"http","scrapeTimeout":"30s","targetLabels":[],"tlsConfig":{}}` | Configuration for the Prometheus ServiceMonitor | 82 | | metrics.serviceMonitor.enabled | bool | `false` | Enable or disable the creation of a ServiceMonitor resource | 83 | | metrics.serviceMonitor.interval | string | `"30s"` | Interval at which metrics should be scraped | 84 | | metrics.serviceMonitor.labels | object | `{}` | Labels to add to the ServiceMonitor resource | 85 | | metrics.serviceMonitor.metricRelabelings | list | `[]` | Relabeling rules for the metrics before ingestion | 86 | | metrics.serviceMonitor.namespace | string | `""` | Namespace where the ServiceMonitor resource should be created. Defaults to Release.Namespace | 87 | | metrics.serviceMonitor.path | string | `"/metrics"` | Path to scrape for metrics | 88 | | metrics.serviceMonitor.port | int | `8096` | Port to scrape for metrics | 89 | | metrics.serviceMonitor.relabelings | list | `[]` | Relabeling rules for the scraped metrics | 90 | | metrics.serviceMonitor.scheme | string | `"http"` | Scheme to use for scraping metrics (http or https) | 91 | | metrics.serviceMonitor.scrapeTimeout | string | `"30s"` | Timeout for scraping metrics | 92 | | metrics.serviceMonitor.targetLabels | list | `[]` | Target labels to add to the scraped metrics | 93 | | metrics.serviceMonitor.tlsConfig | object | `{}` | TLS configuration for scraping metrics | 94 | | nameOverride | string | `""` | Override the default name of the chart. | 95 | | networkPolicy | object | `{"egress":{"allowAllEgress":true,"allowDNS":true,"customRules":[],"dnsNamespace":"kube-system","dnsPodSelector":{"k8s-app":"kube-dns"},"restrictedEgress":{"allowInCluster":true,"allowMetadata":true,"allowedCIDRs":[]}},"enabled":false,"ingress":{"allowExternal":true,"customRules":[],"namespaceSelector":{},"podSelector":{}},"metrics":{"namespace":"","podSelector":{"app.kubernetes.io/name":"prometheus"}},"policyTypes":["Ingress","Egress"]}` | Network Policy configuration for network isolation and security. Requires a CNI plugin that supports NetworkPolicy (Calico, Cilium, Weave, etc.). WARNING: NetworkPolicy cannot be enabled when hostNetwork is used (DLNA mode). The chart will fail with an error if both are enabled simultaneously. | 96 | | networkPolicy.egress | object | `{"allowAllEgress":true,"allowDNS":true,"customRules":[],"dnsNamespace":"kube-system","dnsPodSelector":{"k8s-app":"kube-dns"},"restrictedEgress":{"allowInCluster":true,"allowMetadata":true,"allowedCIDRs":[]}}` | Egress rules configuration - controls what external connections Jellyfin can make. | 97 | | networkPolicy.egress.allowAllEgress | bool | `true` | Allow all egress traffic (internet access for metadata, subtitles, images). When true, Jellyfin can connect to any external destination (0.0.0.0/0). This is the recommended default as Jellyfin needs internet access for: - Downloading movie/TV show metadata (TMDB, TheTVDB, OMDb) - Fetching poster images, fanart, and other artwork - Downloading subtitles (OpenSubtitles) - Updating plugins When false, you must configure restrictedEgress or customRules. | 98 | | networkPolicy.egress.allowDNS | bool | `true` | Allow DNS resolution (required for Jellyfin to function). This adds an egress rule for kube-system namespace with kube-dns pods. DNS is required for resolving metadata provider domains and subtitle services. It is highly recommended to keep this enabled. | 99 | | networkPolicy.egress.customRules | list | `[]` | Additional custom egress rules. Allows for complex scenarios not covered by the standard template. These rules are added as-is to the NetworkPolicy egress section. Example - allow connections to specific database: customRules: - to: - podSelector: matchLabels: app: postgresql ports: - protocol: TCP port: 5432 | 100 | | networkPolicy.egress.dnsNamespace | string | `"kube-system"` | DNS namespace where DNS service is running. Usually "kube-system" but can differ in some Kubernetes distributions. | 101 | | networkPolicy.egress.dnsPodSelector | object | `{"k8s-app":"kube-dns"}` | DNS pod selector labels. Default selector is for kube-dns, but CoreDNS or other DNS providers may use different labels. Adjust if needed. Common alternatives: k8s-app: kube-dns (default) k8s-app: coredns app.kubernetes.io/name: coredns | 102 | | networkPolicy.egress.restrictedEgress | object | `{"allowInCluster":true,"allowMetadata":true,"allowedCIDRs":[]}` | Restricted egress mode for security-conscious deployments. Only used when allowAllEgress is false. Provides fine-grained control over outbound connections. | 103 | | networkPolicy.egress.restrictedEgress.allowInCluster | bool | `true` | Allow communication within the cluster (pod-to-pod). Useful if Jellyfin needs to connect to other services in the cluster. This allows connections to any pod in any namespace. | 104 | | networkPolicy.egress.restrictedEgress.allowMetadata | bool | `true` | Allow HTTPS (443/TCP) for metadata providers. Most metadata providers (TMDB, TheTVDB, OpenSubtitles, Fanart.tv) use HTTPS, so this covers the majority of use cases. This allows connections to any IP on port 443. | 105 | | networkPolicy.egress.restrictedEgress.allowedCIDRs | list | `[]` | Additional IP CIDR blocks to allow egress. Useful for allowing specific IP ranges for metadata providers or other external services. Example - allow entire internet except private networks: allowedCIDRs: - 0.0.0.0/0 Example - allow specific metadata provider IP ranges: allowedCIDRs: - 13.224.0.0/14 # CloudFront (used by many CDNs) | 106 | | networkPolicy.enabled | bool | `false` | Enable NetworkPolicy for the Jellyfin pod. By default, this is disabled to maintain backward compatibility. When enabled, you can control which pods can access Jellyfin (ingress) and what external connections Jellyfin can make (egress). | 107 | | networkPolicy.ingress | object | `{"allowExternal":true,"customRules":[],"namespaceSelector":{},"podSelector":{}}` | Ingress rules configuration - controls which pods/namespaces can access Jellyfin. | 108 | | networkPolicy.ingress.allowExternal | bool | `true` | Allow external access from any namespace and any pod. When true, any pod in the cluster can access Jellyfin (default behavior). When false, only pods matching podSelector/namespaceSelector can access. Set to false for production environments to restrict access. | 109 | | networkPolicy.ingress.customRules | list | `[]` | Additional custom ingress rules. Allows for complex scenarios not covered by the standard template. These rules are added as-is to the NetworkPolicy ingress section. Example - allow from monitoring namespace: customRules: - from: - namespaceSelector: matchLabels: name: monitoring ports: - protocol: TCP port: 8096 | 110 | | networkPolicy.ingress.namespaceSelector | object | `{}` | Namespace selector to allow cross-namespace ingress. Only used when allowExternal is false. Allows you to specify which namespaces can access Jellyfin. Example - only allow from ingress-nginx namespace: namespaceSelector: matchLabels: name: ingress-nginx | 111 | | networkPolicy.ingress.podSelector | object | `{}` | Custom pod selector for allowed ingress traffic. Only used when allowExternal is false. Allows you to specify which pods can access Jellyfin based on labels. Example - only allow pods with specific label: podSelector: matchLabels: jellyfin-client: "true" | 112 | | networkPolicy.metrics | object | `{"namespace":"","podSelector":{"app.kubernetes.io/name":"prometheus"}}` | Prometheus metrics scraping configuration. Automatically allows ingress from Prometheus when metrics.serviceMonitor.enabled is true. This ensures Prometheus can scrape metrics without additional configuration. | 113 | | networkPolicy.metrics.namespace | string | `""` | Namespace where Prometheus is running. Leave empty to use the same namespace as Jellyfin. Set to the monitoring namespace if Prometheus is in a different namespace. Example: "monitoring" or "prometheus" | 114 | | networkPolicy.metrics.podSelector | object | `{"app.kubernetes.io/name":"prometheus"}` | Pod selector for Prometheus pods. These labels must match your Prometheus deployment. The chart will automatically add an ingress rule for pods matching these labels. Default selector works with prometheus-operator and kube-prometheus-stack. Adjust if your Prometheus uses different labels. | 115 | | networkPolicy.policyTypes | list | `["Ingress","Egress"]` | Policy types to enforce. Both ingress and egress policies can be enabled. See: https://kubernetes.io/docs/concepts/services-networking/network-policies/#policy-types | 116 | | nodeSelector | object | `{}` | Node selector for pod scheduling. | 117 | | persistence.cache.accessMode | string | `"ReadWriteOnce"` | PVC specific settings, only used if type is 'pvc'. | 118 | | persistence.cache.annotations | object | `{}` | Custom annotations to be added to the PVC | 119 | | persistence.cache.enabled | bool | `false` | set to false to use emptyDir | 120 | | persistence.cache.hostPath | string | `""` | Path on the host node for cache storage, only used if type is 'hostPath'. | 121 | | persistence.cache.size | string | `"10Gi"` | | 122 | | persistence.cache.storageClass | string | `""` | If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. | 123 | | persistence.cache.type | string | `"pvc"` | Type of volume for cache storage (pvc, hostPath, emptyDir). If 'enabled' is false, 'emptyDir' is used regardless of this setting. | 124 | | persistence.config.accessMode | string | `"ReadWriteOnce"` | | 125 | | persistence.config.annotations | object | `{}` | Custom annotations to be added to the PVC | 126 | | persistence.config.enabled | bool | `true` | set to false to use emptyDir | 127 | | persistence.config.size | string | `"5Gi"` | | 128 | | persistence.config.storageClass | string | `""` | If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. | 129 | | persistence.media.accessMode | string | `"ReadWriteOnce"` | PVC specific settings, only used if type is 'pvc'. | 130 | | persistence.media.annotations | object | `{}` | Custom annotations to be added to the PVC | 131 | | persistence.media.enabled | bool | `true` | set to false to use emptyDir | 132 | | persistence.media.hostPath | string | `""` | Path on the host node for media storage, only used if type is 'hostPath'. | 133 | | persistence.media.size | string | `"25Gi"` | | 134 | | persistence.media.storageClass | string | `""` | If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. | 135 | | persistence.media.type | string | `"pvc"` | Type of volume for media storage (pvc, hostPath, emptyDir). If 'enabled' is false, 'emptyDir' is used regardless of this setting. | 136 | | podAnnotations | object | `{}` | Annotations to add to the pod. | 137 | | podLabels | object | `{}` | Additional labels to add to the pod. | 138 | | podPrivileges | object | `{"hostIPC":false,"hostNetwork":false,"hostPID":false}` | Privileged pod settings for advanced use cases | 139 | | podPrivileges.hostIPC | bool | `false` | Enable hostIPC namespace. Required for NVIDIA MPS (Multi-Process Service) GPU sharing. See: https://docs.nvidia.com/deploy/mps/index.html | 140 | | podPrivileges.hostNetwork | bool | `false` | Enable hostNetwork. Allows pod to use the host's network namespace. | 141 | | podPrivileges.hostPID | bool | `false` | Enable hostPID namespace. Allows pod to see processes on the host. | 142 | | podSecurityContext | object | `{}` | Security context for the pod. | 143 | | priorityClassName | string | `""` | Define a priorityClassName for the pod. | 144 | | readinessProbe | object | `{"httpGet":{"path":"/health","port":"http"},"initialDelaySeconds":10}` | Configure readiness probe for Jellyfin. This probe is disabled during startup (startup probe handles initial checks). Uses httpGet for compatibility with both IPv4 and IPv6. | 145 | | replicaCount | int | `1` | Number of Jellyfin replicas to start. Should be left at 1. | 146 | | resources | object | `{}` | Resource requests and limits for the Jellyfin container. | 147 | | revisionHistoryLimit | int | `3` | Number of old ReplicaSets to retain for rollback history. Set to 0 to disable revision history (not recommended). If not specified, Kubernetes defaults to 10. See: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#revision-history-limit | 148 | | runtimeClassName | string | `""` | Define a custom runtimeClassName for the pod. | 149 | | securityContext | object | `{}` | Security context for the container. | 150 | | service.annotations | object | `{}` | Annotations for the service. | 151 | | service.ipFamilies | list | `[]` | Supported IP families (IPv4, IPv6). Examples: IPv4 only: ["IPv4"] IPv6 only: ["IPv6"] Dual-stack (IPv4 primary): ["IPv4", "IPv6"] Dual-stack (IPv6 primary): ["IPv6", "IPv4"] Note: When using IPv6, ensure your health checks are compatible (consider using httpGet instead of tcpSocket) | 152 | | service.ipFamilyPolicy | string | `""` | Configure dual-stack IP family policy. See: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ Options: SingleStack, PreferDualStack, RequireDualStack For IPv6-only clusters, use "SingleStack" with ipFamilies: ["IPv6"] For dual-stack, use "PreferDualStack" or "RequireDualStack" with ipFamilies: ["IPv4", "IPv6"] or ["IPv6", "IPv4"] | 153 | | service.labels | object | `{}` | Labels for the service. | 154 | | service.loadBalancerClass | string | `""` | Class of the LoadBalancer. | 155 | | service.loadBalancerIP | string | `""` | Specific IP address for the LoadBalancer. | 156 | | service.loadBalancerSourceRanges | list | `[]` | Source ranges allowed to access the LoadBalancer. | 157 | | service.port | int | `8096` | Port for the Jellyfin service. | 158 | | service.portName | string | `"service"` | Name of the port in the service. | 159 | | service.type | string | `"ClusterIP"` | Service type (ClusterIP, NodePort, or LoadBalancer). | 160 | | serviceAccount | object | `{"annotations":{},"automount":true,"create":true,"name":""}` | Service account configuration. See: https://kubernetes.io/docs/concepts/security/service-accounts/ | 161 | | serviceAccount.annotations | object | `{}` | Annotations for the service account. | 162 | | serviceAccount.automount | bool | `true` | Automatically mount API credentials for the service account. | 163 | | serviceAccount.create | bool | `true` | Specifies whether to create a service account. | 164 | | serviceAccount.name | string | `""` | Custom name for the service account. If left empty, the name will be autogenerated. | 165 | | startupProbe | object | `{"failureThreshold":30,"initialDelaySeconds":0,"periodSeconds":10,"tcpSocket":{"port":"http"}}` | Configure startup probe for Jellyfin. This probe gives Jellyfin enough time to start, especially with large media libraries. After the startup probe succeeds once, liveness and readiness probes take over. | 166 | | tolerations | list | `[]` | Tolerations for pod scheduling. | 167 | | volumeMounts | list | `[]` | Additional volume mounts for the Jellyfin container. | 168 | | volumes | list | `[]` | Additional volumes to mount in the Jellyfin pod. | 169 | 170 | ---------------------------------------------- 171 | Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) 172 | 173 | ## Gateway API HTTPRoute 174 | 175 | This chart supports the Kubernetes Gateway API HTTPRoute resource as a modern alternative to Ingress. 176 | 177 | To use HTTPRoute, you need to have Gateway API CRDs installed in your cluster and a Gateway resource configured. 178 | 179 | Example configuration: 180 | 181 | ```yaml 182 | httpRoute: 183 | enabled: true 184 | annotations: {} 185 | parentRefs: 186 | - name: my-gateway 187 | namespace: gateway-system 188 | sectionName: https 189 | hostnames: 190 | - jellyfin.example.com 191 | rules: 192 | - matches: 193 | - path: 194 | type: PathPrefix 195 | value: / 196 | ``` 197 | 198 | For more information about Gateway API, see: 199 | 200 | ## Hardware acceleration 201 | 202 | Out of the box the pod does not have the necessary permissions to enable hardware acceleration (HWA) in Jellyfin. 203 | Adding the following Helm values should make it enable you to use hardware acceleration features. 204 | Some settings may need to be tweaked depending on the type of device (Intel/AMD/NVIDIA/...) and your container runtime. 205 | 206 | Please refer to the Jellyfin upstream documentation for more information about hardware acceleration: 207 | 208 | ```yaml 209 | securityContext: 210 | capabilities: 211 | add: 212 | - "SYS_ADMIN" 213 | drop: 214 | - "ALL" 215 | privileged: false 216 | 217 | extraVolumes: 218 | - name: hwa 219 | hostPath: 220 | path: /dev/dri 221 | 222 | extraVolumeMounts: 223 | - name: hwa 224 | mountPath: /dev/dri 225 | ``` 226 | 227 | ## Network Security 228 | 229 | Jellyfin chart supports Kubernetes NetworkPolicy for network isolation and security hardening. NetworkPolicy allows you to control which pods can access Jellyfin (ingress) and what external connections Jellyfin can make (egress). 230 | 231 | ### Requirements 232 | 233 | - **CNI Plugin**: NetworkPolicy requires a Container Network Interface (CNI) plugin that supports NetworkPolicies, such as: 234 | - Calico 235 | - Cilium 236 | - Weave Net 237 | - Canal 238 | 239 | Check with your cluster administrator if NetworkPolicy is supported in your cluster. 240 | 241 | - **DLNA Incompatibility**: NetworkPolicy cannot be enabled when `enableDLNA: true` or `podPrivileges.hostNetwork: true` is set, as pods using `hostNetwork` bypass NetworkPolicy rules. The chart will fail deployment with a clear error message if both are enabled. 242 | 243 | ### Basic Usage 244 | 245 | By default, NetworkPolicy is disabled to maintain backward compatibility. To enable basic network isolation: 246 | 247 | ```yaml 248 | networkPolicy: 249 | enabled: true 250 | ``` 251 | 252 | This will create a NetworkPolicy with the following defaults: 253 | - **Ingress**: Allow connections from any pod in any namespace 254 | - **Egress**: Allow DNS resolution and all internet access (required for metadata) 255 | 256 | ### Production Configuration - Restrict to Ingress Controller 257 | 258 | For production environments, you typically want to restrict access to only allow traffic through the Ingress controller: 259 | 260 | ```yaml 261 | networkPolicy: 262 | enabled: true 263 | ingress: 264 | allowExternal: false 265 | namespaceSelector: 266 | matchLabels: 267 | name: ingress-nginx 268 | podSelector: 269 | matchLabels: 270 | app.kubernetes.io/name: ingress-nginx 271 | 272 | ingress: 273 | enabled: true 274 | className: nginx 275 | hosts: 276 | - host: jellyfin.example.com 277 | paths: 278 | - path: / 279 | pathType: Prefix 280 | ``` 281 | 282 | ### High Security Configuration - Restricted Egress 283 | 284 | For security-conscious deployments that need to limit outbound connections: 285 | 286 | ```yaml 287 | networkPolicy: 288 | enabled: true 289 | ingress: 290 | allowExternal: false 291 | podSelector: 292 | matchLabels: 293 | jellyfin-client: "true" # Only pods with this label can access 294 | egress: 295 | allowDNS: true # Always needed 296 | allowAllEgress: false # Block unrestricted internet 297 | restrictedEgress: 298 | allowMetadata: true # Allow HTTPS/443 for metadata providers (TMDB, etc.) 299 | allowInCluster: false # Block pod-to-pod communication 300 | ``` 301 | 302 | **Note**: With this configuration, Jellyfin can only: 303 | - Resolve DNS queries 304 | - Connect to HTTPS (port 443) endpoints for metadata providers 305 | - Cannot connect to other pods in the cluster 306 | - Cannot access non-HTTPS services 307 | 308 | ### Monitoring Integration 309 | 310 | If you're using Prometheus for monitoring, the chart automatically allows ingress from Prometheus pods when metrics are enabled: 311 | 312 | ```yaml 313 | networkPolicy: 314 | enabled: true 315 | metrics: 316 | enabled: true 317 | serviceMonitor: 318 | enabled: true 319 | ``` 320 | 321 | By default, the chart allows ingress from pods with label `app.kubernetes.io/name: prometheus`. If your Prometheus uses different labels, customize the selector: 322 | 323 | ```yaml 324 | networkPolicy: 325 | enabled: true 326 | metrics: 327 | namespace: monitoring # If Prometheus is in a different namespace 328 | podSelector: 329 | app: my-prometheus 330 | ``` 331 | 332 | ### Advanced Configuration 333 | 334 | #### Multiple Namespaces Access 335 | 336 | Allow access from multiple namespaces using custom rules: 337 | 338 | ```yaml 339 | networkPolicy: 340 | enabled: true 341 | ingress: 342 | allowExternal: false 343 | customRules: 344 | # Frontend namespace 345 | - from: 346 | - namespaceSelector: 347 | matchLabels: 348 | name: frontend 349 | podSelector: 350 | matchLabels: 351 | access-jellyfin: "true" 352 | ports: 353 | - protocol: TCP 354 | port: 8096 355 | 356 | # Admin tools namespace 357 | - from: 358 | - namespaceSelector: 359 | matchLabels: 360 | name: admin-tools 361 | ports: 362 | - protocol: TCP 363 | port: 8096 364 | ``` 365 | 366 | #### Custom Egress Rules 367 | 368 | Allow connections to specific external services: 369 | 370 | ```yaml 371 | networkPolicy: 372 | enabled: true 373 | egress: 374 | allowAllEgress: false 375 | restrictedEgress: 376 | allowMetadata: true 377 | allowedCIDRs: 378 | - 10.0.0.0/8 # Internal network 379 | - 192.168.0.0/16 # Another internal network 380 | customRules: 381 | # Allow connection to external database 382 | - to: 383 | - ipBlock: 384 | cidr: 203.0.113.0/24 385 | ports: 386 | - protocol: TCP 387 | port: 5432 388 | ``` 389 | 390 | ### Security Considerations 391 | 392 | 1. **Metadata Providers**: Jellyfin requires internet access to download metadata (movie posters, descriptions, etc.) from: 393 | - TheMovieDB (api.themoviedb.org) 394 | - TheTVDB (api.thetvdb.com) 395 | - OpenSubtitles (api.opensubtitles.com) 396 | - Fanart.tv (fanart.tv) 397 | 398 | If you use `restrictedEgress.allowMetadata: true`, these will work as they all use HTTPS (port 443). 399 | 400 | 2. **DNS Access**: DNS resolution is critical for Jellyfin operation. The chart prevents accidental DNS blocking by defaulting `allowDNS: true`. 401 | 402 | 3. **Local Metadata**: If you want to completely block internet access, you can use local metadata (NFO files and local images). This requires manual setup and is not the default Jellyfin behavior. 403 | 404 | 4. **Testing**: Always test NetworkPolicy changes in a development environment first. Misconfigured policies can block legitimate traffic. 405 | 406 | ### Troubleshooting 407 | 408 | **Jellyfin can't download metadata/images:** 409 | - Check that `egress.allowAllEgress: true` or `restrictedEgress.allowMetadata: true` is set 410 | - Verify DNS egress is allowed: `egress.allowDNS: true` 411 | 412 | **Can't access Jellyfin web interface:** 413 | - Verify ingress rules allow traffic from your access point (Ingress controller, LoadBalancer, etc.) 414 | - Check NOTES.txt after deployment for detailed NetworkPolicy status 415 | 416 | **Prometheus can't scrape metrics:** 417 | - Ensure `metrics.enabled: true` and `metrics.serviceMonitor.enabled: true` 418 | - Verify `networkPolicy.metrics.podSelector` matches your Prometheus labels 419 | - Set `networkPolicy.metrics.namespace` if Prometheus is in a different namespace 420 | 421 | **Deployment fails with "NetworkPolicy cannot be enabled...":** 422 | - You have both `networkPolicy.enabled: true` and `hostNetwork: true` (or `enableDLNA: true`) 423 | - NetworkPolicy doesn't work with host networking 424 | - Either disable NetworkPolicy or disable host networking 425 | 426 | For more configuration options, see the full values documentation in [values.yaml](values.yaml). 427 | ## Troubleshooting 428 | 429 | ### inotify Instance Limit Reached 430 | 431 | **Problem:** Jellyfin crashes with error: 432 | ``` 433 | System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached 434 | ``` 435 | 436 | **Root cause:** The Linux kernel has a limit on inotify instances (file system watchers) per user. Jellyfin uses inotify to monitor media libraries for changes. 437 | 438 | **Proper solution (recommended):** 439 | 440 | Increase the inotify limit on the Kubernetes nodes: 441 | 442 | ```bash 443 | # Temporary (until reboot) 444 | sysctl -w fs.inotify.max_user_instances=512 445 | 446 | # Permanent 447 | echo "fs.inotify.max_user_instances=512" >> /etc/sysctl.conf 448 | sysctl -p 449 | ``` 450 | 451 | Recommended values: 452 | - `fs.inotify.max_user_instances`: 512 or higher 453 | - `fs.inotify.max_user_watches`: 524288 or higher (if you have large media libraries) 454 | 455 | **Workaround (if you cannot modify host settings):** 456 | 457 | If you're running on a managed Kubernetes cluster where you cannot modify node-level settings, you can force Jellyfin to use polling instead of inotify. **Note: This is less efficient and may increase CPU usage and delay change detection.** 458 | 459 | ```yaml 460 | jellyfin: 461 | env: 462 | - name: DOTNET_USE_POLLING_FILE_WATCHER 463 | value: "1" 464 | ``` 465 | 466 | This workaround disables inotify file watching in favor of periodic polling, which doesn't require inotify instances but is less efficient. 467 | 468 | ## IPv6 Configuration 469 | 470 | This chart supports IPv6 and dual-stack networking configurations out of the box. Health probes use httpGet by default for compatibility with both IPv4 and IPv6. 471 | 472 | ### IPv6-only Configuration 473 | 474 | For IPv6-only clusters: 475 | 476 | ```yaml 477 | service: 478 | ipFamilyPolicy: SingleStack 479 | ipFamilies: 480 | - IPv6 481 | ``` 482 | 483 | ### Dual-stack Configuration 484 | 485 | For dual-stack clusters (both IPv4 and IPv6): 486 | 487 | ```yaml 488 | service: 489 | ipFamilyPolicy: PreferDualStack # or RequireDualStack 490 | ipFamilies: 491 | - IPv4 492 | - IPv6 # First family in the list is the primary 493 | ``` 494 | 495 | For more information about Kubernetes dual-stack networking, see: 496 | --------------------------------------------------------------------------------