Jellyfin

Media server that streams the shared library to every screen in the house

Jellyfin serves the library to players around the house. It is pinned to the media node so transcodes always hit local SSD.

Manifests & templates

templates/jellyfin/jellyfin-rollout.yaml

Jellyfin Rollout

Show manifest
{{- $projectName  := .Values.global.projectName -}}
{{- $imagePullSecretsName  := .Values.global.imagePullSecretsName -}}
{{- $hostMediaMountPath  := .Values.global.hostMediaMountPath -}}
{{- $configBasePath := .Values.global.configBasePath -}}
{{- $timeZone := .Values.global.timeZone -}}
{{- $userID := .Values.global.userID -}}
{{- with .Values.applications.jellyfin }}
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: {{ .name  }}
  namespace: {{ $projectName }}
spec:
  replicas: 1
  strategy:
    canary:
      maxSurge: 0
      maxUnavailable: 1
      steps:
      - setWeight: 100
  selector:
    matchLabels:
      app: {{ .name  }}
  template:
    metadata:
      labels:
        app: {{ .name  }}
    spec:
      {{- include "media-systems.nodePlacement" $ | nindent 6 }}
      serviceAccount: {{ $projectName }}-service-account
      serviceAccountName: {{ $projectName }}-service-account
      automountServiceAccountToken: true
      containers:
      - name: {{ .name  }}
        image: {{ .image  }}
        imagePullPolicy: IfNotPresent
        lifecycle:
          postStart:
            exec:
              command:
                - "/bin/sh"
                - "-c"
                - |
                    # Temp installation of jellyfin-ffmpeg6
                    apt update && apt install jellyfin-ffmpeg6 -y;
                    echo done;
        ports:
        - name: {{ .name  | substr 0 9 }}-http
          containerPort: {{ .ports.http }}
          protocol: TCP
        env:
        - name: JELLYFIN_PublishedServerUrl
          value: {{ .name  }}.opi5cluster.co.uk
        - name: PGID
          value: {{ $userID | quote }}
        - name: PUID
          value:  {{ $userID | quote }}
        - name: TZ
          value: {{ $timeZone | quote }}
        volumeMounts:
        - name: {{ .name  }}-config
          mountPath: /config
        - name: {{ .name  }}-movies
          mountPath: {{ .moviesMount }}
        - name: {{ .name  }}-series
          mountPath: {{ .seriesMount }}
        - name: {{ .name  }}-anime
          mountPath: {{ .animeMount }}
        - name: {{ .name  }}-others
          mountPath: {{ .othersMount }}
        resources: {}
        securityContext:
          privileged: true
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      volumes:
      - name: {{ .name }}-config
        hostPath:
          path: {{ $configBasePath }}/{{ .name }}
          type: DirectoryOrCreate
      - name: {{ .name }}-movies
        hostPath:
          path: {{ $hostMediaMountPath }}/movies
          type: Directory
      - name: {{ .name }}-series
        hostPath:
          path: {{ $hostMediaMountPath }}/series
          type: Directory
      - name: {{ .name }}-anime
        hostPath:
          path: {{ $hostMediaMountPath }}/anime
          type: Directory
      - name: {{ .name }}-others
        hostPath:
          path: {{ $hostMediaMountPath }}/others
          type: Directory
{{- end -}}

templates/jellyfin/jellyfin-lb-service.yaml

Jellyfin LoadBalancer Service (MetalLB IP, pinned to the media node)

Show manifest
{{- $projectName  := .Values.global.projectName }}
{{- with .Values.applications.jellyfin }}
apiVersion: v1
kind: Service
metadata:
  name: {{ .name }}-loadbalancer
  namespace: {{ $projectName }}
  annotations:
    metallb.universe.tf/loadBalancerIPs:  {{ .LoadBalancerIP }}
spec:
  type: LoadBalancer
  selector:
    app: {{ .name }}
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
    - name: {{ .name }}
      protocol: TCP
      port: 80
      targetPort: {{ .ports.http  }}
{{- end -}}

templates/_shared-templates/http-route-template.yaml

Shared HTTPRoute template (used by every media app)

Show manifest
{{- $projectName  := .Values.global.projectName }}
{{- range $app, $property := .Values.applications }}
{{- if ne $app "whisper" }}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: {{ $app }}-httproute
  namespace: {{ $projectName  }}
  annotations:
    link.argocd.argoproj.io/external-link: "https://{{ $app }}.opi5cluster.co.uk"
spec:
  parentRefs:
    - name: istio-gateway
      namespace: istio
      sectionName: websecure
  hostnames:
    - {{ $app }}.opi5cluster.co.uk
  rules:
    - backendRefs:
        - name: {{ $app }}-service
          port: {{ $property.ports.http }}
{{- end }}
{{- end }}

templates/_shared-templates/service-template.yaml

Shared Service template (used by every media app)

Show manifest
{{- $projectName  := .Values.global.projectName }}
{{- range $app, $property := .Values.applications }}
---
apiVersion: v1
kind: Service
metadata:
  name: {{ $app }}-service
  namespace: {{ $projectName }}
spec:
  type: ClusterIP
  selector:
    app: {{ $app }}
  ipFamilyPolicy: SingleStack
  ports:
    - name: {{ $app | substr 0 9 }}-http
      protocol: TCP
      port: {{  $property.ports.http }}
      targetPort: {{  $property.ports.http }}
    {{- if eq $property.name "qbittorrent" }}
    - name: {{ $app }}-tcp
      protocol: TCP
      port: {{ $property.ports.torrent }}
      targetPort: {{ $property.ports.torrent }}
    - name: {{ $app }}-udp
      protocol: UDP
      port: {{ $property.ports.torrent }}
      targetPort: {{ $property.ports.torrent }}
    {{- end }}
    {{- if eq $property.name "tdarr" }}
    - name: {{ $app }}-server
      protocol: TCP
      port: {{ $property.ports.server }}
      targetPort: {{ $property.ports.server }}
    {{- end }}
{{- end }}

templates/_shared-templates/service-account.yaml

Shared ServiceAccount (used by every media app)

Show manifest
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ .Values.global.projectName }}-service-account
  namespace: {{ .Values.global.projectName }}

templates/_shared-templates/apps_configs-secret.yaml

Shared apps-configs Secret template (used by every media app)

Show manifest
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: apps-configs-external-secret
  namespace: {{ .Values.global.projectName}}
spec:
  refreshInterval: 24h0m0s
  secretStoreRef:
    kind: ClusterSecretStore
    name: vault-cluster-secret-store
  target:
    name: apps-configs-secret
    creationPolicy: Owner
    template:
      engineVersion: v2
      templateFrom:
      - configMap:
          name: bazarr-configs-config-tpl
          items:
          - key: bazarr_config
      - configMap:
          name: prowlarr-configs-config-tpl
          items:
          - key: prowlarr_config
      - configMap:
          name: qbittorrent-configs-config-tpl
          items:
          - key: qbittorrent_config
      - configMap:
          name: radarr-configs-config-tpl
          items:
          - key: radarr_config
      - configMap:
          name: sonarr-configs-config-tpl
          items:
          - key: sonarr_config
  data:
    - secretKey: bazarr_api_key
      remoteRef:
        key: media-systems
        property: bazarr-api-key
    - secretKey: bazarr_db_name
      remoteRef:
        key: media-systems
        property: bazarr-db-name
    - secretKey: flask_secret_key
      remoteRef:
        key: media-systems
        property: flask-secret-key
    - secretKey: main_password
      remoteRef:
        key: media-systems
        property: main-password
    - secretKey: main_username
      remoteRef:
        key: media-systems
        property: main-username
    - secretKey: prowlarr_api_key
      remoteRef:
        key: media-systems
        property: prowlarr-api-key
    - secretKey: prowlarr_db_name
      remoteRef:
        key: media-systems
        property: prowlarr-db-name
    - secretKey: qbittorrent_password
      remoteRef:
        key: media-systems
        property: qbittorrent-password
    - secretKey: subdl_api_key
      remoteRef:
        key: media-systems
        property: subdl-api-key
    - secretKey: radarr_api_key
      remoteRef:
        key: media-systems
        property: radarr-api-key
    - secretKey: radarr_db_name
      remoteRef:
        key: media-systems
        property: radarr-db-name
    - secretKey: sonarr_api_key
      remoteRef:
        key: media-systems
        property: sonarr-api-key
    - secretKey: sonarr_db_name
      remoteRef:
        key: media-systems
        property: sonarr-db-name

    - secretKey: db_host_name
      remoteRef:
        key: postgres
        property: internal_host_name
    - secretKey: db_username
      remoteRef:
        key: postgres
        property: user_name
    - secretKey: db_password
      remoteRef:
        key: postgres
        property: user_password
    - secretKey: db_port
      remoteRef:
        key: postgres
        property: port
  • media-systems is one umbrella Application: the whole pipeline deploys as a unit, pinned to a single node with local SSD storage. Per-app settings live in the media chart, not here.
  • This app is a section of the shared media-systems values file above; the media stack deploys as one ArgoCD Application.

Trade-offs

Decision. Pin the pipeline to one node with local SSD

Alternative. Spreading it across nodes with network storage

Why. Transcode workloads are latency-sensitive; local disk keeps playback smooth and the pin makes storage claims simple.

← Back to Media · All service groups