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.
- Latest version:
1.0.1, published - Source chart:
https://jellyfin.github.io/jellyfin-helm/jellyfin:3.7.0@sha256:d649534398b4e04f08ab6a38f7344f09ac7d6d9767dc000820c7e61d9f7a8b8b - Proof level:
deploys, compared with the chart by the kadet-hub/recipes CI renderspassed in the release run of with check 0.18.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade F 378 findings, one per advisory and package, in the 1 image of version 1.0.1: CRITICAL 1, HIGH 65, MEDIUM 138, LOW 174, UNKNOWN 0. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/jellyfin/jellyfin.
What it deploys
recipes.jellyfin(init.yml)recipes.jellyfin.jellyfin(jellyfin.yml):jellyfin
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
1.0.1, :sha256:1ca2045dbba3e29deb013daaff12fa9d804042ce1f4cda7b1ad2c421078c7a131.0.0, :sha256:0fd983b365f9361800a6b7f0990b173c776bfdb7c11e626303373e42e37ef7d4
The recipe's files are in kadet-hub/recipes. All recipes