Setting up Jellyfin on Kubernetes
In the Battle of the Home Media Systems it turned out that Jellifin is the best alternative for an open source HMS.
Up and running in Minikube
First off we write a deployment manifest. A deployment might be a bit overkill for exploring in Minikube, but I do intend to reuse it for my own cluster later. Lets use one replica for now, as we don’t know if Jellyfin can scale horizontally out of the box. Next thing we need to know is wat ports Jellyfin uses. This information is easily found in the Jellyfin docs. To be able to access the Jellyfin instance inside Minikube we also need a Nodeport service. I also add a neamspace. IT makes the cleanup easier and faster than tearing down and reinitializing minikube.
Now to apply the manifests and find the nodeport address for Jellyfin.
kubectl apply -f manifest.yaml
minikube service -n media-server jellyfin-service
On the inside
We are first greeted by a welcoming page and the entrypoint to a configuration wizard. Lest click this through. Not that we want to clickops, by to get a feel for what configuration options we are presented with. Looks like the configurations we entered in the setup ended up in the /config directory in some way. The /config directory also houses a sql lite database.
Custom image
Looks like the proper way to install plugins without using a UI is by placing them in the Jellyfin plugin directory, which in the official image is located here /config/plugins/
.
Time to build a custom image, but how? We could download and extract the plugins in the dockerfile. It would probably be the simplest solution.
Downside we would need the image to contain curl and every time a plugin is updated every preceding plugin would need to be re-downloaded and installed.
Or we could download the plugins in the CD pipeline and cache it there.
Persistent storage
Although config as code i preferable, there is way to many configuration and too large configuration files that I can be bothered to make configmaps for all of them, at leas for now. Also, it seems that thh config directory is also used for other things like a temporary storage for transcoded files, metadata for media files, log files, the SQLite database, etc, things that configmaps aren’t suited for. To start off we’ll make a volume claim that we mount to /conf. A problem with this approach is that the existing content of /conf will be wiped. That includes the SSO plugin that we copied into the custom image. To the to over come this hurdle we’ll make an init container that will copy the content of /config in the image to the config persistent volume. ‘
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-config-pvc
namespace: media-server
labels:
app: jellyfin
spec:
storageClassName: container-storage
volumeName: jellyfin-config-pv
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
spec:
volumes:
- name: config
persistentVolumeClaim:
claimName: jellyfin-config-pvc
- name: media
persistentVolumeClaim:
claimName: jellyfin-media-pvc
initContainers:
- name: plugin-copy
image: registry.organiccode.net/alex/jellyfin:latest
command: ['cp', '-r', '/config/plugins', '/tmp/']
volumeMounts:
- name: config
mountPath: /tmp
With the config ouf of the way. Let’s create a volume claim for the media.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-media-pvc
namespace: media-server
labels:
app: jellyfin
spec:
storageClassName: container-storage
volumeName: jellyfin-media-pv
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Gi
500Gb will do for now. It is primarily just to test it out. Besides besides, I don’t think resource limits are respected for host path volumes. I will probably change this to a NFS in the future, but this will have to do for now. Lets copy over some video files and see what happens.
Not bad!
SSO - KeyCloak
Having SSO is a must. Who wants to manage user accounts for every single service. Lets go over to the plugin configurations. Administration -> Dashboards -> Plugins -> MyPlugins -> SSO_Auth. Here we find the usual OIDC config fields. To fill them out we first need to create a new client in our realm with client authentication. The generated XML config will look something like this.
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SamlConfigs />
<OidConfigs>
<item>
<key>
<string>KeyCloak</string>
</key>
<value>
<PluginConfiguration>
<OidEndpoint>https://keycloak.organiccode.net/realms/home</OidEndpoint>
<OidClientId>jellyfin</OidClientId>
<OidSecret>************</OidSecret>
<Enabled>true</Enabled>
<EnableAuthorization>true</EnableAuthorization>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
<AdminRoles>
<string>admin</string>
</AdminRoles>
<Roles />
<EnableFolderRoles>false</EnableFolderRoles>
<EnableLiveTvRoles>false</EnableLiveTvRoles>
<EnableLiveTv>false</EnableLiveTv>
<EnableLiveTvManagement>false</EnableLiveTvManagement>
<LiveTvRoles />
<LiveTvManagementRoles />
<FolderRoleMappings />
<OidScopes />
<NewPath>true</NewPath>
<CanonicalLinks>
<item>
<key>
<string>alex</string>
</key>
<value>
<guid>b6caa797-28ca-4022-a2db-e28ca375939f</guid>
</value>
</item>
</CanonicalLinks>
<DisableHttps>false</DisableHttps>
<DoNotValidateEndpoints>false</DoNotValidateEndpoints>
<DoNotValidateIssuerName>false</DoNotValidateIssuerName>
</PluginConfiguration>
</value>
</item>
</OidConfigs>
The plugin also requires us to manually add the SSO login button to the login screen through Jellyfins branding configuration. A bit hacky, but a disclaimer on the plugins github readme states that it is 100% in alpha. The branding configuration is found under general settings.
<form action="https://hms.organiccode.net/sso/OID/start/KeyCloak">
<button class="raised block emby-button button-submit">
Sign in with KeyCloak
</button>
</form>
And it works! Except for the admin role mapping which I still need to figure out this has turned out to be a decent piece of software, now enjoyed by the entire family.