Alright, now that we have a Kubernetes cluster running (previous post), we have to install some applications 🚀!

I decided to start with Jellyfin, an open source Media System. It is a good first project since it requires the configuration of a simple deployment, a persistent volume, and a load balancer.

The main configuration is pasted at the end of the article, and a step-by-step guide is presented below.

Volumes

The main deployment contains a few sections, the following defined the Volumes where the media files (movies, series, albums, etc.) are stored. For my current setup—which consists of three VMs running on a macOS host—I mounted the folder containing my media (on the host machine) to all the VMs. All the paths shown in volumePaths are subdirectories of the shared folder, and the name associated with a mountPath corresponds to a volume.

volumeMounts:
  - mountPath: "/Movies"
    readOnly: false
    name: nas
    subPath: Movies
  - mountPath: "/Shows"
    readOnly: false
    name: nas
    subPath: Shows
  - mountPath: "/config"
    readOnly: false
    name: nas
    subPath: config
  - mountPath: "/cache"
    readOnly: false
    name: nas
    subPath: cache
volumes:
  - name: nas
    persistentVolumeClaim:
      claimName: pvc-jellyfin-nas

Notes

  • to mount a folder in a Linux VM, we must install the vmware-tools package (pacman -S open-vm-tools), setup a systemd service, and add an entry to fstab (more details in VMware/Install Arch Linux as a guest).
  • the volume can be a folder, a separate disk, or be full NAS server.

Kubernetes Persistent Volume and Claim

For the drive to be accessible on the cluster pods, we need to create a Persistent Volume (PV) and a persistent Volume Claim (PVC), again, the name (nas) has to match. You can see, on the PV definition that we set hostPath since we are making a local folder (/mnt/nas/jellyfin) available on the cluster.

Those two resources are pretty simple and self explanatory:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-jellyfin-nas
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  hostPath:
    path: "/mnt/nas/jellyfin"

and the claim:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-jellyfin-nas
  namespace: jellyfin
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi

the option ReadWriteMany means that the volume can be mounted as read-write by many nodes. The size of the request has to match the volume capacity so they can be assigned!

Retain Configuration

For the Jellyfin server to retain its configuration when the pod restarts—or the cluster is shutdown—we must create and assigned a /config folder (shown above). Otherwise, the folder will be created on the pod, and the settings will be lost when the pod is shutdown. Similarly, the cache/ folder where metadata can be saved.

On my host machine, the jellyfin folder that is mounted to the VMs contains:

> ls
Movies/ Shows/  cache/  config/

LoadBalancer

By default, the server will only be accessible through a ClusterIP, which is only accessible from within the cluster. Kubernetes offer two other types of service to access applications:

  • NodePort: allows for remote access to the cluster node where the application is running
  • LoadBalancer: allows for remote access to the main node and redirect the traffic internally to the correct NodePort

If you are looking for a great video explaining how Kubernetes Services work, I recommend Kubernetes Services explained from TechWorld with Nana.

For our app, we could use a NodePort and making sure our pod is always running on the same node (by adjusting affinity) to have a static IP. But in general, it is recommended to set up a LoadBalancer and only use NodePort for testing during development. Important to point out that when many apps are running on a cloud cluster, it might become more cost effective to setup a NGINX Ingress Controller.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: jellyfin
  name: jellyfin
  namespace: jellyfin
spec:
  ports:
  - name: web-tcp
    port: 8096
    protocol: TCP
    targetPort: 8096
    nodePort: 30000
  - name: web-udp
    port: 8096
    protocol: UDP
    targetPort: 8096
    nodePort: 30001
  selector:
    app: jellyfin
  type: LoadBalancer
    protocol: UDP
    targetPort: 8096
    nodePort: 30001
  selector:
    app: jellyfin

Applying to the cluster and accessing the server

I saved the main deployment file to deployment.yaml, the pv.yaml, and pvc.yaml in a common jellyfin/ directory. The configuration can be applied to the Kubernetes cluster using kubectl apply -f jellyfin/. Once the pod is running, the server should be accessible at the port specify in the LoadBalancer configuration.

First we can see that the main server is running:

> kubectl get pods -n jellyfin
NAME                        READY   STATUS    RESTARTS      AGE
jellyfin-669cbb7dff-mgjwm   1/1     Running   5 (25m ago)   21d

and that the LoadBalancer is redirecting the traffic:

> kubectl get svc -n jellyfin
NAME       TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                         AGE
jellyfin   LoadBalancer   10.96.72.180   <pending>     8096:30000/TCP,8096:30001/UDP   22d

as noted above, the TCP port is used for the web interface, and the UDP for the data transfer. I don’t have an external DNS setup at the moment (EXTERNAL-IP <pending> state), but this could be use to redirect an hostname, e.g. http://homelab.philippemiron.com/jellyfin, directly to the application.

To connect to our interface, we simply navigate to KUBERNETES_CLUSTER_IP:30000 on a device on the same network. After completing the guided configuration (only on first login!), you should have access to your media server.

Note: The naming schemes and directories organization have to follow a predefined pattern to be automatically recognized by Jellyfin. See more details on the documentation (Movies and Shows).

Bonus

If like myself you have a Samsung TV, there is a last step. Since the Jellyfin app is not available in Samsung’s Smart Hub, we must manually installed the application (GitHub jellyfin/jellyfin-tizen).

First, the TV has to be put in Developer’s mode, this is done by:

  1. Open the Smart Hub.
  2. In the “Apps” panel, enter “12345”.
  3. Switch Developer mode to “On”.
  4. Enter the IP of the computer that you will used to connect to the TV.

Then, we have permission to push the Jellyfin app to the TV from the specified IP address. Tim Georift built a Docker image (GitHub Georift/install-jellyfin-tizen) to easily pull and install the latest app (GitHub jellyfin/jellyfin-tizen) to a TV.

> docker run --rm georift/install-jellyfin-tizen <samsung tv ip>

Deployment configuration

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: jellyfin
    app.kubernetes.io/instance: jellyfin
    app.kubernetes.io/name: jellyfin
  name: jellyfin
  namespace: jellyfin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jellyfin
  template:
    metadata:
      labels:
        app: jellyfin
        app.kubernetes.io/name: jellyfin
    spec:
      containers:
      - image: jellyfin/jellyfin
        imagePullPolicy: Always
        name: jellyfin
        ports:
        - containerPort: 8096
          name: web
          protocol: TCP
        env:
        - name: TZ
          value: "America/New York"
        volumeMounts:
        - mountPath: "/Movies"
          readOnly: false
          name: nas
          subPath: Movies
        - mountPath: "/Shows"
          readOnly: false
          name: nas
          subPath: Shows
        - mountPath: "/config"
          readOnly: false
          name: nas
          subPath: config
        - mountPath: "/cache"
          readOnly: false
          name: nas
          subPath: cache
      volumes:
        - name: nas
          persistentVolumeClaim:
            claimName: pvc-jellyfin-nas