Install the k8s cluster if not alreadly installed 1 2 microk8s enable dns storage helm3 microk8s status
Add the Truecharts Repo 1 microk8s helm repo add truecharts https://charts.truecharts.org/
Pull the helm chart 1 microk8s helm pull truecharts/jellyfin
To enable DLNA we need to enable access to the host network. Host Access addon
1 microk8s enable host-access:ip=A.B.C.D
Modify the values.yaml file to set the configuration 1 2 3 tar -xvf ./jellyfin-*.tgz cd jellyfin cat values.yamls
Make the data directory APPLICATION_ROOT_DIRECTORY: Path where the application config and data files are stored
1 2 3 4 mkdir -p APPLICATION_ROOT_DIRECTORY/config mkdir -p APPLICATION_ROOT_DIRECTORY/data sudo chown -R 777 APPLICATION_ROOT_DIRECTORY
Create Persistent Volumes and Persistent Volume Claims Persistent Volumes Config jellyfin-config-pv.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 apiVersion: v1 kind: PersistentVolume metadata: name: jellyfin-config-pv spec: capacity: storage: 100Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage local: path: APPLICATION_ROOT_DIRECTORY/config # This must exist on the host nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - dionysus
1 microk8s kubectl apply -f jellyfin-config-pv.yaml
Data jellyfin-data-pv.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 apiVersion: v1 kind: PersistentVolume metadata: name: jellyfin-data-pv spec: capacity: storage: 5Ti volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage local: path: APPLICATION_ROOT_DIRECTORY/data # This must exist on the host nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - dionysus
1 microk8s kubectl apply -f jellyfin-data-pv.yaml
1 2 3 NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE jellyfin-config-pv 100Gi RWO Retain Bound default/jellyfin-config-pvc local-storage 98s jellyfin-data-pv 5Ti RWO Retain Bound default/jellyfin-data-pvc local-storage 78s
Persistent Volume Claims Config jellyfin-config-pvc.yaml Bonds to jellyfin-config-pv
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: jellyfin-config-pvc spec: storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set accessModes: - ReadWriteOnce volumeName: jellyfin-config-pv resources: requests: storage: 100Gi
1 microk8s kubectl apply -f jellyfin-config-pvc.yaml
Data jellyfin-data-pvc.yaml Bonds to jellyfin-data-pv
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: jellyfin-data-pvc spec: storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set accessModes: - ReadWriteOnce volumeName: jellyfin-data-pv resources: requests: storage: 5Ti
1 microk8s kubectl apply -f jellyfin-data-pvc.yaml
1 microk8s kubectl get pvc
1 2 3 NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE jellyfin-config-pvc Bound jellyfin-config-pv 100Gi RWO local-storage 26s jellyfin-data-pvc Bound jellyfin-data-pv 5Ti RWO local-storage 12s
Modify the values.yaml file to set the configuration We’re going to set the jellyfin configuration within the container.
1 2 cd jellyfin nano values.yaml
DLNA_PORT: Port the dlna server will listen on AUTO_DISCOVER_PORT: DLNA autodiscover port WEB_GUI_PORT: Port to access the web gui
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 image: repository: tccr.io/truecharts/jellyfin pullPolicy: IfNotPresent tag: v10.8.10@sha256:d2c377ee7ea463110a1dda7eb1b231424ad05245aaf95744e76164bd2e593377 broadcastProxyImage: repository: tccr.io/truecharts/socat pullPolicy: IfNotPresent tag: v1.7.4.4@sha256:2417f121a5fd08012c927bfb7cdedb00ce97e0ec53fbf38352be8c6f197f7294 service: main: ports: main: port: WEB_GUI_PORT targetPort: WEB_GUI_PORT autodiscovery: enabled: true ports: autodiscovery: enabled: true protocol: udp port: AUTO_DISCOVER_PORT hostPort: AUTO_DISCOVER_PORT hostIP: A.B.C.D dlna: enabled: true ports: dlna: enabled: true protocol: udp port: DLNA_PORT hostPort: DLNA_PORT hosstIP: A.B.C.D persistence: config: enabled: true mountPath: "/config" type: pvc existingClaim: jellyfin-config-pvc cache: enabled: true mountPath: "/cache" type: "emptyDir" transcode: enabled: true mountPath: "/config/transcodes" type: "emptyDir" media: enabled: true mountPath: /media type: pvc existingClaim: jellyfin-data-pvc portal: open: enabled: true securityContext: container: readOnlyRootFilesystem: false workload: main: podSpec: hostNetwork: true containers: main: env: JELLYFIN_PublishedServerUrl: "{{ $.Values.chartContext.APPURL }}" broadcastproxy: enabled: false type: DaemonSet podSpec: hostNetwork: true # Proxy doesn't seem to respect the TERM signal, so by default # this ends up just hanging until the default grace period ends. # This is unnecesary since this workload only proxies autodiscovery # messages. terminationGracePeriodSeconds: 3 containers: broadcastproxy: enabled: true primary: true imageSelector: broadcastProxyImage securityContext: readOnlyRootFilesystem: true command: ["/bin/sh"] # Quite a lot going on here: # - Resolve Jellyfin's autodiscovery service IP from its FQDN via getent hosts # - Export the IP to `$TARGET_IP` # - Check `$TARGET_IP` is not empty (so we can crash if it is - will help to detect templating errors) # - Touch `/tmp/healty` to use with the readiness, liveness and startup probes # - Start socat in proxy mode # - On exit remove `/tmp/healthy` args: ["-c", "export TARGET_IP=$(getent hosts '{{ printf \"%v-autodiscovery\" (include \"tc.v1.common.lib.chart.names.fullname\" $) }}' | awk '{ print $1 }') && [[ ! -z $TARGET_IP ]] && touch /tmp/healthy && socat UDP-LISTEN:AUTO_DISCOVER_PORT,fork,reu> probes: readiness: enabled: true type: exec command: - cat - /tmp/healthy liveness: enabled: true type: exec command: - cat - /tmp/healthy startup: enabled: true type: exec command: - cat - /tmp/healthy # -- enable Jellyfin autodiscovery on LAN autodiscovery: enabled: true
Install the helm chart with our values.yaml file 1 microk8s helm install jellyfin truecharts/jellyfin --values ./values.yaml
Check the status of the installed application. 1 2 microk8s status microk8s kubectl describe pods
Common Errors Error from server (BadRequest): container in pod is waiting to start: ContainerCreating You probably need to change the permissions on the PV directory. This path is what is written in the PersistentVolume in the path variable. A quick chmod -R 777 to this path will most likely fix the issue. The conatiner should update the permissions once it runs.
Service yaml file Since we are using the hostnetwork we don’t need the an additional service file to route information.
More information herehttps://medium.com/swlh/kubernetes-external-ip-service-type-5e5e9ad62fcd
Firewall Rules This assumes you are using ufw. ufw is bascally a wrapper for IPTABLES. If you have ever used IPTABLES before you understand why ufw exists.https://docs.syncthing.net/users/firewall.html
1 sudo ufw default allow routed
1 2 3 sudo ufw allow from A.B.C.0/24 to any port WEB_GUI_PORT proto tcp sudo ufw allow from A.B.C.0/24 to any port AUTO_DISCOVER_PORT proto udp sudo ufw allow from A.B.C.0/24 to any port DLNA_PORT proto udp
References https://kubernetes.io/docs/concepts/services-networking/service/