Octelium documentation · Latest
Self-Hosting Jellyfin Media Server
Jellyfin is a popular open source media server that lets you stream your own movies, TV shows and music to your browsers, TVs, phones and other devices, and a common self-hosted alternative to Plex and Emby. This guide shows you how to deploy Jellyfin via its official Helm chart on the same underlying Kubernetes cluster that is running the Octelium Cluster, and then serve it as an Octelium Service. Octelium provides the following:
Private client-based access over WireGuard/QUIC via the
octelium connectcommand (read more here) from your laptops and desktops.Public clientless BeyondCorp access (read more here) from any web browser, including mobile browsers, where your Users authenticate via OpenID Connect, SAML 2.0 or GitHub IdentityProviders (read more here) as well as Passkeys before they are able to reach Jellyfin at all.
Access for your TVs and other devices at home that cannot run the
octeliumclient, by publishing the Service to your home network from a single connected device.Serving an existing Jellyfin server running at your home behind NAT without opening any ports.
Identity-based, context-aware access control on a per-request basis via policy-as-code with CEL and OPA (read more about Policies and access control here).
OpenTelemetry-native, identity-based, L7 aware visibility and auditing (read more here).
If you installed the Cluster via the quick installation guide, you can simply use the command export KUBECONFIG="/etc/rancher/k3s/k3s.yaml" in your Cluster VM/VPS before running the kubectl and helm commands below.
Install Jellyfin
In this guide we use the official Jellyfin Helm chart. First, we create a values.yaml file for the chart as follows:
The config volume is a persistent volume claim that stores Jellyfin's database, settings and metadata. The media volume mounts your media library, in this example the /mnt/media directory of the node, at the /media path inside the container. We also mount it as read-only since Jellyfin only needs to read your media files.
A hostPath volume is mainly suitable for a single-node Kubernetes cluster, such as the k3s cluster installed by the quick installation guide, where your media files are located on that node's own disk. If your media is stored elsewhere, such as on a NAS, you can instead create your own persistent volume claim (e.g. an NFS-backed one) and use it via the persistence.media.existingClaim value.
Now install the chart and wait for Jellyfin to become ready as follows:
The chart's default image tag is the Jellyfin version that the chart was released with, which is why we explicitly pin the Jellyfin version via the image.tag value. Note that Jellyfin 12 removed the legacy authorization methods and route prefixes, so very old third-party clients might no longer work. Make sure to read the Jellyfin release notes before upgrading, and to back up the config volume first.
You can also enable hardware-accelerated transcoding by exposing your node's GPU to the container via the chart's volumes, volumeMounts and securityContext values. Read more about hardware acceleration in the Jellyfin docs here.
The chart creates the jellyfin Kubernetes service in the jellyfin Kubernetes namespace which listens over the port 8096.
Create the Service
Now we create an Octelium Service for Jellyfin whose upstream is the jellyfin Kubernetes service. We also define a family Group (read more about Groups here) where only its members are allowed to access the Service as follows:
The highlighted authorizationMode field is required for Jellyfin. Jellyfin clients, including its web app, authenticate to Jellyfin via the Authorization header in the MediaBrowser scheme, while the Service deletes the downstream's Authorization header by default (read more here). Setting the PASS mode passes the header to Jellyfin as is. This does not interfere with the Cluster's own authentication since the Cluster only uses the Authorization header for Bearer tokens, while browsers authenticate to the Cluster via its own cookie.
The Service port is automatically inferred from the upstream URL, which means that the Service also listens over the port 8096 (read more here). Now add your Users to the family Group. Here is an example:
You can now apply the creation of the Group, the Service and the Users via the octeliumctl apply command (read more here) as follows:
Now open Jellyfin, either via the client-based or the clientless mode as shown in the following sections, and complete the setup wizard where you create the Jellyfin admin account and add a library whose folder is /media. Unlike exposing Jellyfin directly to the internet, only your authorized Users are able to reach the setup wizard in the first place.
The Service drops the Forwarded, X-Forwarded-* and X-Real-IP request headers by default (read more here). Therefore, Jellyfin sees every request as coming from the private IP address of the Cluster, which by default belongs to Jellyfin's local network. This means, for example, that Jellyfin's bitrate limit for remote clients does not apply. You can instead set per-user bitrate limits in Jellyfin's user settings.
Client-Based Access
Once connected to the Cluster via the octelium connect command (read more here), you can simply visit Jellyfin from your browser at the URL http://jellyfin:8096. You can also use the same address as the server address in desktop clients such as Jellyfin Media Player.
You can also connect via the rootless mode and map the Service to your host's localhost (read more here) as follows:
TVs and Other Devices at Home
Smart TVs, streaming sticks and game consoles cannot run the octelium client. Instead, you can use a single always-on device in your home network, such as a Raspberry Pi or a home server, to connect to the Cluster and publish the Service to the whole home network (read more about publishing Services here). First, we create a dedicated WORKLOAD User (read more about User types here) for that device as follows:
The inline Policy makes sure that the device's identity can only be used to access the jellyfin Service and nothing else in the Cluster. After applying the User via octeliumctl apply, create an authentication token Credential (read more here) for it as follows:
Now run the octelium client as a container (read more here) on that device and publish the Service to all of its network interfaces as follows:
The container does not need any additional Linux capabilities since it only publishes the Service to its own network namespace via the unprivileged gVisor netstack mode (read more here). Now you can use the address http://<DEVICE_IP>:8096 as the server address in the Jellyfin apps of your TVs and other devices in your home network.
Any device in your home network can now reach Jellyfin through that device, although every device still needs to login with a Jellyfin account. All of such requests are authorized and logged as requests of the living-room User.
Clientless Access
Authorized HUMAN Users can access Jellyfin via the clientless BeyondCorp mode directly from their browsers, including mobile browsers, at the URL https://jellyfin.<DOMAIN> without having to install any client. An unauthenticated User is first redirected to the Cluster's login page in order to authenticate via an IdentityProvider (read more here). There are currently 3 types:
GitHub OAuth IdentityProvider as shown in detail here
OpenID Connect IdentityProviders (e.g. Okta, Auth0, etc...) as shown here.
SAML 2.0 IdentityProviders (e.g. Okta, Entra ID, etc...) as shown here.
Furthermore, you can register a FIDO2 Authenticator (e.g. Yubikeys) in order to directly login later via Passkey (read more here) without having to use an IdentityProvider.
Once authenticated to the Cluster, the User then logs in to Jellyfin itself with their Jellyfin account.
Native Jellyfin apps cannot go through the Cluster's browser-based login. For such apps, you can use the client-based mode or publish the Service to your home network as shown above, while you can use the mobile browser on your phone whenever you are away from home.
Serving Jellyfin from Behind NAT
You do not have to run Jellyfin inside the Cluster. If you already run Jellyfin at home, for example as a Docker container on a NAS that stores your media, while the Cluster runs on a cloud VM, you can remotely serve it via a connected octelium client without opening any ports in your home network (read more about remotely serving upstreams here). First, we create a WORKLOAD User for the home server as follows:
Now we change the upstream of the Service to the local address of Jellyfin on the home server and set the user field to the home-server User as follows:
After applying both via octeliumctl apply, create an authentication token Credential for the home-server User via octeliumctl create cred --user home-server home-server and then connect to the Cluster from the home server and serve the Service via the --serve flag (read more here) as follows:
In this setup, every stream goes from your home server to the Cluster and then to the User. Therefore, the upload bandwidth of your home internet connection limits the quality and number of the streams that you can watch while away from home.
Access Control
Since every request to Jellyfin is authorized by the Service, you can control access in an identity-based and context-aware way on top of Jellyfin's own permissions (read more about Policies and access control here). Here is an example where the members of the family Group can always access Jellyfin, while the members of a kids Group can only access it between 08:00 and 20:00 of the Europe/Berlin time zone:
Since the Service authorizes every single request, any new request of the kids Group members is denied from 20:00 onward, which means that they can no longer browse Jellyfin or start new streams. You can read more about the available CEL functions here.
Visibility
Octelium also provides OpenTelemetry-ready, application-layer L7 aware visibility and access logging in real time (read more about visibility here). Here is an example of an access Log of a video stream request:
Jellyfin clients pass their Jellyfin access token in the ApiKey query parameter of some requests such as streams and WebSocket connections. Access Logs never include the query parameters of the request URI, and sensitive request headers such as Authorization and Cookie are always removed from the logged headers, so Jellyfin's tokens never end up in your logs.
Here are a few more features that you might be interested in: