Jellyfin Podman

Introduction

Sometime ago I was running Plex Media Server running on my Synology DS1817+. After an update, the WebUI and my AndroidTV App stopped displaying my media. At this point, I started looking for other methods of hosting my videos. I moved to Emby, but wanted to move and utilize Free and Open Source Software. I present JellyFin!

Jellyfin became my top choice because it has similar features found in Plex while also being opensource. One of my requirements was running the software on capable hardware to stream my 4K Blu-Rays across my network which meant transcoding for my TVs. Luckily the softare can utilize both CPU and GPU transcoding. Another self-imposed requirement was running via Podman and having a configuration thats portable and repeatable, so that I could easily rebuild/redeploy the server at any moment (another benefit is running on OpenShift).

Utilizing Podman allows me to use a Pod configuration YAML to build my Jellyfin container. The full YAML can be found at the end of this post. In the next section, I’ll be covering the important bits to running JellyFin on my server.

Configuration

To start building our pod manifest for Podman we start with defining a Kubernetes pod.

Pod configuration:

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: jellyfin
  name: jellyfin
spec:
  restartPolicy: Always
  containers:
  - name: server
    image: docker.io/jellyfin/jellyfin:latest
    ports:
    - containerPort: 8096
      hostPort: 8096
      protocol: TCP
    - containerPort: 8920
      hostPort: 8920
      protocol: TCP

To ensure Jellyfin bootstraps and start correctly we need a few environment variables.

Environment, Container:

spec:
  containers:
  - name: server
    image: docker.io/jellyfin/jellyfin:latest
    env:
    - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
      value: "1"
    - name: JELLYFIN_MEDIA_DIR
      value: /media
    - name: JELLYFIN_LOG_DIR
      value: /config/log
    - name: JELLYFIN_CONFIG_DIR
      value: /config/config
    - name: JELLYFIN_WEB_DIR
      value: /jellyfin/jellyfin-web
    - name: LC_ALL
      value: en_US.UTF-8
    - name: LANG
      value: en_US.UTF-8
    - name: JELLYFIN_CACHE_DIR
      value: /cache
    - name: JELLYFIN_FFMPEG
      value: /usr/lib/jellyfin-ffmpeg/ffmpeg
    - name: LANGUAGE
      value: en_US:en
    - name: JELLYFIN_DATA_DIR
      value: /config

I keep my Media on a Synology DS1817+, and serve the media via NFS mount to my server.

Media Location, Volume:

spec:
  volumes:
  - hostPath:
      path: /mnt/nfs/media
      type: Directory
    name: mnt-nfs-media

The following is used to tell the container on how to mount the NFS mount.

Media Location, Container Mount:

spec:
  containers:
  - name: server
    image: docker.io/jellyfin/jellyfin:latest
    volumeMounts:
    - mountPath: /media
      name: mnt-nfs-media-host-0
    - mountPath: /cache
      name: jellyfin-cache-pvc
    - mountPath: /config
      name: jellyfin-config-pvc

Podman also allows use of the nVidia GPUs by passing the following Environment variables.

nVidia Container Enablement:

spec:
  containers:
  - name: server
    image: docker.io/jellyfin/jellyfin:latest
    env:
    - name: NVIDIA_VISIBLE_DEVICES
      value: all
    - name: NVIDIA_DRIVER_CAPABILITIES
      value: all

Conclusion

As long as the media is hosted, using a containerized Jellyfin provides the flexabilty of moving the service where you can host it. The caution is the "platform" should be powerful enough to stream or transcode the media you want to watch. A Raspberry Pi 4 should be able to stream 1080p; my Synology DS1817+ could (sometimes) stream/transcode 4K video, but not well most of the times. Currently, I’m running my Jellyfin pod on a Dell R720 w/ nVidia Tesla M40 GPU, but due to a few quirks with the nVidia driver, I have disabled nVidia CUDA transcoding. The following code is the full original YAML manifest that I’ve used.

Full Configuration

# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-3.4.1-dev
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: jellyfin
  name: jellyfin
spec:
  containers:
  - command:
    - /jellyfin/jellyfin
    env:
    - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
      value: "1"
    - name: JELLYFIN_MEDIA_DIR
      value: /media
    - name: JELLYFIN_LOG_DIR
      value: /config/log
    - name: JELLYFIN_CONFIG_DIR
      value: /config/config
    - name: JELLYFIN_WEB_DIR
      value: /jellyfin/jellyfin-web
    - name: LC_ALL
      value: en_US.UTF-8
    - name: LANG
      value: en_US.UTF-8
    - name: JELLYFIN_CACHE_DIR
      value: /cache
    - name: JELLYFIN_FFMPEG
      value: /usr/lib/jellyfin-ffmpeg/ffmpeg
    - name: LANGUAGE
      value: en_US:en
    - name: JELLYFIN_DATA_DIR
      value: /config
    - name: NVIDIA_VISIBLE_DEVICES
      value: all
    - name: NVIDIA_DRIVER_CAPABILITIES
      value: all
    image: docker.io/jellyfin/jellyfin:latest
    name: server
    ports:
    - containerPort: 8096
      hostPort: 8096
      protocol: TCP
    - containerPort: 8920
      hostPort: 8920
      protocol: TCP
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities:
        drop:
        - CAP_MKNOD
        - CAP_AUDIT_WRITE
      privileged: false
      readOnlyRootFilesystem: false
      seLinuxOptions: {}
    tty: true
    volumeMounts:
    - mountPath: /media
      name: mnt-nfs-media-host-0
    - mountPath: /cache
      name: jellyfin-cache-pvc
    - mountPath: /config
      name: jellyfin-config-pvc
    workingDir: /
  dnsConfig: {}
  restartPolicy: Always
  volumes:
  - hostPath:
      path: /mnt/nfs/media
      type: Directory
    name: mnt-nfs-media-host-0
  - name: jellyfin-cache-pvc
    persistentVolumeClaim:
      claimName: jellyfin-cache
  - name: jellyfin-config-pvc
    persistentVolumeClaim:
      claimName: jellyfin-config