k8s-mediaserver-operator

March 5, 2024 ยท View on GitHub

Your all-in-one resource for your media needs!

I am so happy to announce the first release of the k8s-mediaserver-operator, a project that mixes up some of the mainstream tools for your media needs.

The Custom Resource that is implemented, allows you to create a fully working and complete media server on #kubernetes, based on:

Plex Media Server - A complete and fully funtional mediaserver that allows you to render in a webUI your movies, TV Series, podcasts, video streams.

Jellyfin - It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps

Sonarr - A TV series and show tracker, that allows the integration with download managers for searching and retrieving TV Series, organizing them, schedule notifications when an episode comes up and much more.

Radarr - The same a Sonarr, but for movies!

Jackett - An API interface that keeps easy your life interacting with trackers for torrents.

Prowlarr - An indexer manager/proxy built on the popular *arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers.

Transmission - A fast, easy and reliable torrent client.

Sabnzbd - A free and easy binary newsreader.

All container images used by the operator are from linuxserver

Each of the components can be enabled or disabled if you already have something in place in your lab!

Introduction

I started working on this project because I was tired of using the 'containerized' version with docker/podman-compose, and I wanted to experiment a bit both with helm and operators.

It is quite simple to use, and very minimalistic, with customizations that are strictly related to usability and access, rather than complex customizations, even if, maybe in the future, we could add it!

Each container has its init container in order to initialize configurations on the PV before starting the actual pod and avoid to restart the pods.

QuickStart

The operator and the CR are already configured with some defaults settings to make you jump and go with it.

All you need is:

  • A namespace where you want to put your Custom Resource and all the pods that will spawn
  • Being able to provision an RWX PV where to store configurations, downloads, and all related stuff (suggested > 200GB). Persistent Volume or StorageClasses for dynamically provisioned volumes are REQUIRED (See below for NFS)
  1. First install the CRD and the operator:

AMD/Intel:

kubectl apply -f k8s-mediaserver-operator.yml

ARM - Raspberry Pi:

kubectl apply -f k8s-mediaserver-operator-arm64.yml
  1. Install the custom resource with the default values:
kubectl apply -f k8s-mediaserver.yml

In seconds, you will be ready to use your applications!

With default settings, your applications will run in these paths:

ServiceLink
Sonarrhttp://k8s-mediaserver.k8s.test/sonarr
Radarrhttp://k8s-mediaserver.k8s.test/radarr
Transmissionhttp://k8s-mediaserver.k8s.test/transmission
Jacketthttp://k8s-mediaserver.k8s.test/jackett
Prowlarrhttp://k8s-mediaserver.k8s.test/prowlarr
Jellyfinhttp://k8s-jelly.k8s.test/
PLEXhttp://k8s-plex.k8s.test/
  1. (Optional) Use custom values:

If you want to use your custom setup for all the services:

  • Copy the default values files cp ./helm-charts/k8s-mediaserver/values.yaml my-values.yaml
  • Make all the changes you want in the new file ./helm-charts/k8s-mediaserver/my-values.yaml

With this value saved in the top level directory of this repo, running the below will add the resources to your cluster, under the helm release name k8s-mediaserver

helm install -f my-values.yaml k8s-mediaserver ./helm-charts/k8s-mediaserver/

To make changes to the deploy

helm upgrade -f my-values.yaml k8s-mediaserver ./helm-charts/k8s-mediaserver/

This is equivalent to running mount {SERVER-IP}:/mount/path/on/nfs/server ... in each container, where ... is different per resource, as defined in the templates directory (for each resource). In addition to the above, you should also edit your subpaths so that they when they are appended to your path: specified in values.yaml, they map to the directories you intend.

The mediaserver CR

The CR is quite simple to configure, and I tried to keep the number of parameters low to avoid confusion, but still letting some customization to fit the resource inside your cluster.

General config

Config pathMeaningDefault
general.ingress_hostThe hostname to use in ingress definition, this will be the hostname where the applications will be exposedk8s-mediaserver.k8s.test
general.plex_ingress_hostThe hostname to use for PLEX as it must be exposed on a / dedicated pathk8s-plex.k8s.test
general.jellyfin_ingress_hostThe hostname to use for JellyFin as it must be exposed on a / dedicated pathk8s-jelly.k8s.test
general.image_tagThe name of the image tag (arm32v7-latest, arm64v8-latest, development)latest
general.pgidThe GID for the process1000
general.puidThe UID for the process1000
general.nodeSelectorDefault Node Selector for all the pods. Per-service nodeSelectors are merged against this.{}
general.storage.customVolumeFlag if you want to supply your own volume and not use a PVCfalse
general.storage.pvcNameName of the persistenVolumeClaim configured in deploymentsmediaserver-pvc
general.storage.accessModeAccess mode for mediaserver PVC in case of single nodesReadWriteMany
general.storage.pvcStorageClassSpecifies a storageClass for the PVC""
general.storage.sizeSize of the persistenVolume50Gi
general.storage.subPaths.tvDefault subpath for tv series folder on used storagemedia/tv
general.storage.subPaths.moviesDefault subpath for movies folder on used storagemedia/movies
general.storage.subPaths.downloadsDefault root path for downloads for both sabnzbd and transmission on used storagedownloads
general.storage.subPaths.transmissionDefault subpath for transmission downloads on used storagegeneral.storage.subPaths.downloads/transmission
general.storage.subPaths.sabnzbdDefault subpath for sabnzbd downloads on used storagegeneral.storage.subPaths.downloads/sabnzbd
general.storage.subPaths.configDefault subpath for all config files on used storageconfig
general.storage.volumesSupply custom volume to be mounted for all services{}
general.ingress.ingressClassNameReference an IngressClass resource that contains additional Ingress configuration""

Plex

Config pathMeaningDefault
plex.enabledFlag if you want to enable Plextrue
plex.claimIMPORTANT Token from your account, needed to claim the serverCHANGEME
plex.replicaCountNumber of replicas serving Plex1
plex.container.nodeSelectorNode Selector for the Plex pods{}
plex.container.portThe port in use by the container32400
plex.container.imageThe image used by the containerdocker.io/linuxserver/plex
plex.container.tagThe tag used by the containernull
plex.service.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer)ClusterIP
plex.service.portThe port assigned to the service32400
plex.service.nodePortIn case of service.type NodePort, the nodePort to use""
plex.service.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
plex.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
plex.ingress.enabledIf true, creates the ingress resource for the applicationtrue
plex.ingress.annotationsAdditional field for annotations, if needed{}
plex.ingress.pathThe path where the application is exposed/plex
plex.ingress.tls.enabledIf true, tls is enabledfalse
plex.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
plex.resourcesLimits and Requests for the container{}
plex.volumeIf set, Plex will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Jellyfin

Config pathMeaningDefault
jellyfin.enabledFlag if you want to enable Jellyfintrue
jellyfin.replicaCountNumber of replicas serving Jellyfin1
jellyfin.container.nodeSelectorNode Selector for the Jellyfin pods{}
jellyfin.container.portThe port in use by the container8096
jellyfin.container.imageThe image used by the containerdocker.io/linuxserver/jellyfin
jellyfin.container.tagThe tag used by the containernull
jellyfin.service.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer)ClusterIP
jellyfin.service.portThe port assigned to the service8096
jellyfin.service.nodePortIn case of service.type NodePort, the nodePort to use""
jellyfin.service.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
jellyfin.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
jellyfin.ingress.enabledIf true, creates the ingress resource for the applicationtrue
jellyfin.ingress.annotationsAdditional field for annotations, if needed{}
jellyfin.ingress.pathThe path where the application is exposed/jellyfin
jellyfin.ingress.tls.enabledIf true, tls is enabledfalse
jellyfin.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
jellyfin.resourcesLimits and Requests for the container{}
jellyfin.volumeIf set, Jellyfin will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Sonarr

Config pathMeaningDefault
sonarr.enabledFlag if you want to enable Sonarrtrue
sonarr.container.portThe port in use by the container8989
sonarr.container.nodeSelectorNode Selector for the Sonarr pods{}
sonarr.container.imageThe image used by the containerdocker.io/linuxserver/sonarr
sonarr.container.tagThe tag used by the containernull
sonarr.service.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer)ClusterIP
sonarr.service.portThe port assigned to the service8989
sonarr.service.nodePortIn case of service.type NodePort, the nodePort to use""
sonarr.service.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
sonarr.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
sonarr.ingress.enabledIf true, creates the ingress resource for the applicationtrue
sonarr.ingress.annotationsAdditional field for annotations, if needed{}
sonarr.ingress.pathThe path where the application is exposed/sonarr
sonarr.ingress.tls.enabledIf true, tls is enabledfalse
sonarr.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
sonarr.resourcesLimits and Requests for the container{}
sonarr.volumeIf set, Plex will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Radarr

Config pathMeaningDefault
radarr.enabledFlag if you want to enable Radarrtrue
radarr.container.portThe port in use by the container7878
radarr.container.nodeSelectorNode Selector for the Radarr pods{}
radarr.container.imageThe image used by the containerdocker.io/linuxserver/radarr
radarr.container.tagThe tag used by the containernull
radarr.service.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer)ClusterIP
radarr.service.portThe port assigned to the service7878
radarr.service.nodePortIn case of service.type NodePort, the nodePort to use""
radarr.service.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
radarr.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
radarr.ingress.enabledIf true, creates the ingress resource for the applicationtrue
radarr.ingress.annotationsAdditional field for annotations, if needed{}
radarr.ingress.pathThe path where the application is exposed/radarr
radarr.ingress.tls.enabledIf true, tls is enabledfalse
radarr.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
radarr.resourcesLimits and Requests for the container{}
radarr.volumeIf set, Plex will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Jackett

Config pathMeaningDefault
jackett.enabledFlag if you want to enable Jacketttrue
jackett.container.portThe port in use by the container9117
jackett.container.nodeSelectorNode Selector for the Jackett pods{}
jackett.container.imageThe image used by the containerdocker.io/linuxserver/jackett
jackett.container.tagThe tag used by the containernull
jackett.service.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer)ClusterIP
jackett.service.portThe port assigned to the service9117
jackett.service.nodePortIn case of service.type NodePort, the nodePort to use""
jackett.service.extraLBServiceIf true, it creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
jackett.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
jackett.ingress.enabledIf true, creates the ingress resource for the applicationtrue
jackett.ingress.annotationsAdditional field for annotations, if needed{}
jackett.ingress.pathThe path where the application is exposed/jackett
jackett.ingress.tls.enabledIf true, tls is enabledfalse
jackett.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
jackett.resourcesLimits and Requests for the container{}
jackett.volumeIf set, Plex will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Prowlarr

Config pathMeaningDefault
prowlarr.enabledFlag if you want to enable Prowlarrtrue
prowlarr.container.portThe port in use by the container9117
prowlarr.container.nodeSelectorNode Selector for the Prowlarr pods{}
prowlarr.container.imageThe image used by the containerdocker.io/linuxserver/prowlarr
prowlarr.container.tagThe tag used by the containerdevelop
prowlarr.service.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer)ClusterIP
prowlarr.service.portThe port assigned to the service9117
prowlarr.service.nodePortIn case of service.type NodePort, the nodePort to use""
prowlarr.service.extraLBServiceIf true, it creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
prowlarr.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
prowlarr.ingress.enabledIf true, creates the ingress resource for the applicationtrue
prowlarr.ingress.annotationsAdditional field for annotations, if needed{}
prowlarr.ingress.pathThe path where the application is exposed/prowlarr
prowlarr.ingress.tls.enabledIf true, tls is enabledfalse
prowlarr.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
prowlarr.resourcesLimits and Requests for the container{}
prowlarr.volumeIf set, Plex will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Transmission

Config pathMeaningDefault
transmission.enabledFlag if you want to enable Transmissiontrue
transmission.container.port.utpThe port in use by the container9091
transmission.container.nodeSelectorNode Selector for the Transmission pods{}
transmission.container.port.peerThe port in use by the container for peer connection51413
transmission.container.imageThe image used by the containerdocker.io/linuxserver/transmission
transmission.container.tagThe tag used by the containernull
transmission.service.utp.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer) for Transmission itselfClusterIP
transmission.service.utp.portThe port assigned to the service for Transmission itself9091
transmission.service.utp.nodePortIn case of service.type NodePort, the nodePort to use for Transmission itself""
transmission.service.utp.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
transmission.service.peer.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer) for peer portClusterIP
transmission.service.peer.portThe port assigned to the service for peer port51413
transmission.service.peer.nodePortIn case of service.type NodePort, the nodePort to use for peer port""
transmission.service.peer.nodePortUDPIn case of service.type NodePort, the nodePort to use for peer port UDP service""
transmission.service.peer.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
transmission.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
transmission.ingress.enabledIf true, creates the ingress resource for the applicationtrue
transmission.ingress.annotationsAdditional field for annotations, if needed{}
transmission.ingress.pathThe path where the application is exposed/transmission
transmission.ingress.tls.enabledIf true, tls is enabledfalse
transmission.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
transmission.config.auth.enabledEnables authentication for Transmissionfalse
transmission.config.auth.usernameUsername for Transmission""
transmission.config.auth.passwordPassword for Transmission""
transmission.resourcesLimits and Requests for the container{}
transmission.volumeIf set, Plex will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Sabnzbd

Config pathMeaningDefault
sabnzbd.enabledFlag if you want to enable Sabnzbdtrue
sabnzbd.container.nodeSelectorNode Selector for the Sabnzbd pods{}
sabnzbd.container.port.httpThe port in use by the container8080
sabnzbd.container.port.httpsThe port in use by the container for peer connection9090
sabnzbd.container.imageThe image used by the containerdocker.io/linuxserver/sabnzbd
sabnzbd.container.tagThe tag used by the containernull
sabnzbd.service.http.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer) for Sabnzbd itselfClusterIP
sabnzbd.service.http.portThe port assigned to the service for Sabnzbd itself9091
sabnzbd.service.http.nodePortIn case of service.type NodePort, the nodePort to use for Sabnzbd itself""
sabnzbd.service.http.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
sabnzbd.service.https.typeThe kind of Service (ClusterIP/NodePort/LoadBalancer) for https portClusterIP
sabnzbd.service.https.portThe port assigned to the service for peer port51413
sabnzbd.service.https.nodePortIn case of service.type NodePort, the nodePort to use for https port""
sabnzbd.service.https.extraLBServiceIf true, creates an additional LoadBalancer service with '-lb' suffix (requires a cloud provider or metalLB)false
sabnzbd.service.extraLBService.annotationsInstead of using extraLBService as a bool, you can use it as a map to define annotations on the loadbalancernull
sabnzbd.ingress.enabledIf true, creates the ingress resource for the applicationtrue
sabnzbd.ingress.annotationsAdditional field for annotations, if needed{}
sabnzbd.ingress.pathThe path where the application is exposed/sabnzbd
sabnzbd.ingress.tls.enabledIf true, tls is enabledfalse
sabnzbd.ingress.tls.secretNameName of the secret holding certificates for the secure ingress""
sabnzbd.resourcesLimits and Requests for the container{}
sabnzbd.volumeIf set, Plex will create a PVC for it's config volume, else it will be put on general.storage.subPaths.config{}

Helpful use-cases

Using a cluster-external NFS server

This assumes that you have a pre-configured NFS server set up on your network that is accessible from all nodes. If it is not accessible by all nodes, pods will not enter ready state when scheduled on nodes that do not have NFS access.

To add an NFS volume to each resource, edit the K8SMediaServer CR to match below snipttet. You should change the server: and path: values to match your NFS.

general:
  storage:
    customVolume: true
    volumes:
      nfs:
        server: { SERVER-IP }
        path: /mount/path/on/nfs/server/

Adding annotations to the extra load balancer

If you need an extra load balancer on any service, you can either enable it like this:

plex:
  service:
    extraLBService: true

or like this, if you need to add annotations to it (for use with cloud providers to configure the load balancer for example) :

plex:
  service:
    extraLBService:
      annotations:
        service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags:

About the project

This project is intended as an exercise, and absolutely for fun. This is not intended to promote piracy.

Also feel free to contribute and extend it!