kadet-hub logokadet-hub

Jellyfin on Kubernetes without Helm, with Kapitan

The Jellyfin 12.4 media server with its web client and API on port 8096, rendered by the kadet-hub kubernetes generator 1.14.0 from the Jellyfin project’s Helm chart jellyfin 3.7.0 with its default values.

What it deploys

Required parameters

No parameter has to be set.

Include

A target includes the recipe and its options as classes and can override anything the recipe sets:

classes:
  - recipes.jellyfin
parameters:
  namespace: jellyfin
  kapitan:
    vars:
      target: jellyfin

Fetching it

The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.

A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.1:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/jellyfin:1.0.1@sha256:1ca2045dbba3e29deb013daaff12fa9d804042ce1f4cda7b1ad2c421078c7a13
        output_path: inventory/classes/recipes/jellyfin

The first command fetches the recipe into inventory/classes/recipes/jellyfin, the second the generators and compiles:

kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch

The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/jellyfin@sha256:1ca2045dbba3e29deb013daaff12fa9d804042ce1f4cda7b1ad2c421078c7a13 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.

Settings

<c> stands for components.jellyfin.

Chart value Inventory
image.tag <c>.image, tag and digest
persistence.config.size <c>.persistent_volume_claims.jellyfin-config.storage
persistence.media.size <c>.persistent_volume_claims.jellyfin-media.storage
persistence.*.storageClass <c>.persistent_volume_claims.<claim>.storage_class
jellyfin.env <c>.env, a mapping of name to value
resources <c>.resources
service.type generators.kubernetes.services.jellyfin.type
nodeSelector, tolerations, affinity <c>.node_selector, <c>.tolerations, <c>.affinity
podSecurityContext, securityContext <c>.workload_security_context, <c>.security_context
volumes, volumeMounts <c>.volumes.<name>, <c>.volume_mounts.<name>
startupProbe.failureThreshold <c>.healthcheck.startup.failure_threshold

Hardware transcoding is not tested, because kind has no GPU. The chart’s README enables it with the capability SYS_ADMIN in securityContext and the host’s /dev/dri as a hostPath volume. In the recipe these are <c>.security_context, <c>.volumes and <c>.volume_mounts.

The chart’s Ingress, HTTPRoute, NetworkPolicy, ServiceMonitor with its metrics hook, and DLNA on the host network are off by default and not part of the recipe.

Any list set in the target is appended to the recipe’s list. To replace one, set the key to ~ in a class included after the recipe and the new list in the target.

Proof

check jellyfin --level deploys with tests/values.yaml and tests/target.yml, on kind for each Kubernetes minor CI tests. The five objects differ in two ways. The Deployment and the Service select on name, the chart on app.kubernetes.io/name and instance, and both select the same pods. The pod sets automountServiceAccountToken: true, which the chart leaves unset while its ServiceAccount allows the token. The chart binds no role.

tests/smoke calls Jellyfin through the Service and the API server’s service proxy. /health answers Healthy. /System/Info/Public, which answers without a login, is JSON whose Version is the Deployment’s image tag. Jellyfin reports the tag 12.4 as 12.4.0, and the test accepts either form.

Versions

The recipe's files are in kadet-hub/recipes. All recipes