For a long time, I’ve been on the hunt for a comprehensive and well-crafted tutorial to deploy a media server on my Kubernetes cluster. This media server stack includes Jellyfin, Radarr, Sonarr, Jackett, and qBittorrent. Let’s briefly dive into what each component brings to our setup
Application | Description |
---|---|
Jellyfin | An open-source media system that provides a way to manage and stream your media library across various devices. |
Radarr | A movie collection manager for Usenet and BitTorrent users. It automates the process of searching for movies, downloading, and managing your movie library. |
Sonarr | Similar to Radarr but for TV shows. It keeps track of your series, downloads new episodes, and manages your collection with ease. |
Jackett | Acts as a proxy server, translating queries from other apps (like Sonarr or Radarr) into queries that can be understood by a wide array of torrent search engines. |
qBittorrent | A powerful BitTorrent client that handles your downloads. Paired with Jackett, it streamlines finding and downloading media content. |
Gluetun | A lightweight, open-source VPN client for Docker environments, supporting multiple VPN providers to secure and manage internet connections across containerized applications. It ensures privacy and seamless network security with easy configuration and integration. |
The configuration for these applications is hosted on Longhorn storage, ensuring resilience and ease of management, while the media (movies, shows, books, etc.) is stored on a Synology NAS DS223. The NAS location is utilized as a Persistent Volume (PV) through NFS 4.1 by Kubernetes.
In this tutorial, you’ll find the Kubernetes configuration for each necessary component to set up, install, and secure each service used by the media server.
Synology NAS NFS Setup for Kubernetes#
If you use Synology NAS, this is the rule I created for my NFS share which will be mounted on kubernetes side.
Let’s start step by step.
Configuring PVC and PV for NFS Share#
Media
#
Create nfs-media-pv-and-pvc.yaml:
1apiVersion: v1
2kind: PersistentVolume
3metadata:
4 name: jellyfin-videos
5spec:
6 capacity:
7 storage: 400Gi
8 accessModes:
9 - ReadWriteOnce
10 nfs:
11 path: /volume1/server/k3s/media
12 server: storage.merox.cloud
13 persistentVolumeReclaimPolicy: Retain
14 mountOptions:
15 - hard
16 - nfsvers=3
17 storageClassName: ""
18# Persistent Volume spec including capacity, access modes, NFS path, and server details follow
19---
20apiVersion: v1
21kind: PersistentVolumeClaim
22metadata:
23 name: jellyfin-videos
24 namespace: media
25spec:
26 accessModes:
27 - ReadWriteOnce
28 resources:
29 requests:
30 storage: 400Gi
31 volumeName: jellyfin-videos
32 storageClassName: ""
33# Persistent Volume Claim spec including access modes, resources requests, and storage class name follow
kubectl apply -f nfs-media-pv-and-pvc.yaml
Download
#
Create nfs-download-pv-and-pvc.yaml:
1apiVersion: v1
2kind: PersistentVolume
3metadata:
4 name: qbitt-download
5spec:
6 capacity:
7 storage: 400Gi
8 accessModes:
9 - ReadWriteOnce
10 nfs:
11 path: /volume1/server/k3s/media/download
12 server: storage.merox.cloud
13 persistentVolumeReclaimPolicy: Retain
14 mountOptions:
15 - hard
16 - nfsvers=3
17 storageClassName: ""
18# Persistent Volume spec including capacity, access modes, NFS path, and server details follow
19---
20apiVersion: v1
21kind: PersistentVolumeClaim
22metadata:
23 name: qbitt-download
24 namespace: media
25spec:
26 accessModes:
27 - ReadWriteOnce
28 resources:
29 requests:
30 storage: 400Gi
31 volumeName: qbitt-download
32 storageClassName: ""
33# Persistent Volume Claim spec including access modes, resources requests, and storage class name follow
Apply with:
kubectl apply -f nfs-download-pv-and-pvc.yaml
Configuring Longhorn PVC for Each Application#
Create app-config-pvc.yaml:
1apiVersion: v1
2kind: PersistentVolumeClaim
3metadata:
4 name: app # radarr for example
5 namespace: media
6spec:
7 accessModes:
8 - ReadWriteOnce
9 storageClassName: longhorn
10 resources:
11 requests:
12 storage: 5Gi
13# Persistent Volume Claim spec including access modes, storage class name, and resources requests follow
Apply with:
kubectl apply -f app-config-pvc.yaml
Deploying each application#
Jellyfin
#
Jellyfin serves as our media streaming platform, providing access to movies, TV shows, and other media across various devices. Here’s how to deploy it
Create specific yaml for each file, for example: radarr-deployment.yaml Apply with
kubectl apply -f radarr-deployment.yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: jellyfin
5 namespace: media
6spec:
7 replicas: 1
8 selector:
9 matchLabels:
10 app: jellyfin
11 template:
12 metadata:
13 labels:
14 app: jellyfin
15 spec:
16 containers:
17 - name: jellyfin
18 image: jellyfin/jellyfin
19 volumeMounts:
20 - name: config
21 mountPath: /config
22 - name: videos
23 mountPath: /data/videos
24 ports:
25 - containerPort: 8096
26 volumes:
27 - name: config
28 persistentVolumeClaim:
29 claimName: jellyfin-config
30 - name: videos
31 persistentVolumeClaim:
32 claimName: jellyfin-videos
Sonarr
#
Sonarr automates TV show downloads, managing our series collection efficiently.
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: sonarr
5 namespace: media
6spec:
7 replicas: 1
8 selector:
9 matchLabels:
10 app: sonarr
11 template:
12 metadata:
13 labels:
14 app: sonarr
15 spec:
16 containers:
17 - name: sonarr
18 image: linuxserver/sonarr
19 env:
20 - name: PUID
21 value: "1057"
22 - name: PGID
23 value: "1056"
24 volumeMounts:
25 - name: config
26 mountPath: /config
27 - name: videos
28 mountPath: /tv
29 - name: downloads
30 mountPath: /downloads
31 ports:
32 - containerPort: 8989
33 volumes:
34 - name: config
35 persistentVolumeClaim:
36 claimName: sonarr-config
37 - name: videos
38 persistentVolumeClaim:
39 claimName: jellyfin-videos
40 - name: downloads
41 persistentVolumeClaim:
42 claimName: qbitt-download
Radarr
#
Radarr works like Sonarr but focuses on movies, keeping our film library organized and up-to-date.
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: radarr
5 namespace: media
6spec:
7 replicas: 1
8 selector:
9 matchLabels:
10 app: radarr
11 template:
12 metadata:
13 labels:
14 app: radarr
15 spec:
16 containers:
17 - name: radarr
18 image: linuxserver/radarr
19 env:
20 - name: PUID
21 value: "1057"
22 - name: PGID
23 value: "1056"
24 volumeMounts:
25 - name: config
26 mountPath: /config
27 - name: videos
28 mountPath: /movies
29 - name: downloads
30 mountPath: /downloads
31 ports:
32 - containerPort: 7878
33 volumes:
34 - name: config
35 persistentVolumeClaim:
36 claimName: radarr-config
37 - name: videos
38 persistentVolumeClaim:
39 claimName: jellyfin-videos
40 - name: downloads
41 persistentVolumeClaim:
42 claimName: qbitt-download
Jackett
#
Jackett acts as a bridge between torrent search engines and our media management tools, enhancing their capabilities.
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: jackett
5 namespace: media
6spec:
7 replicas: 1
8 selector:
9 matchLabels:
10 app: jackett
11 template:
12 metadata:
13 labels:
14 app: jackett
15 spec:
16 containers:
17 - name: jackett
18 image: linuxserver/jackett
19 env:
20 - name: PUID
21 value: "1057"
22 - name: PGID
23 value: "1056"
24 volumeMounts:
25 - name: config
26 mountPath: /config
27 ports:
28 - containerPort: 9117
29 volumes:
30 - name: config
31 persistentVolumeClaim:
32 claimName: jackett-config
qBittorrent#
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: qbittorrent
5 namespace: media
6spec:
7 replicas: 1
8 selector:
9 matchLabels:
10 app: qbittorrent
11 template:
12 metadata:
13 labels:
14 app: qbittorrent
15 spec:
16 containers:
17 - name: qbittorrent
18 image: linuxserver/qbittorrent
19 resources:
20 limits:
21 memory: "2Gi"
22 requests:
23 memory: "512Mi"
24 env:
25 - name: PUID
26 value: "1057"
27 - name: PGID
28 value: "1056"
29 volumeMounts:
30 - name: config
31 mountPath: /config
32 - name: downloads
33 mountPath: /downloads
34 ports:
35 - containerPort: 8080
36 volumes:
37 - name: config
38 persistentVolumeClaim:
39 claimName: qbitt-config
40 - name: downloads
41 persistentVolumeClaim:
42 claimName: qbitt-download
qBittorrent with Gluetun#
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: qbittorrent
5 namespace: media
6spec:
7 replicas: 1
8 selector:
9 matchLabels:
10 app: qbittorrent
11 template:
12 metadata:
13 labels:
14 app: qbittorrent
15 spec:
16 containers:
17 - name: qbittorrent
18 image: linuxserver/qbittorrent
19 resources:
20 limits:
21 memory: "2Gi"
22 requests:
23 memory: "512Mi"
24 env:
25 - name: PUID
26 value: "1057"
27 - name: PGID
28 value: "1056"
29 volumeMounts:
30 - name: config
31 mountPath: /config
32 - name: downloads
33 mountPath: /downloads
34 ports:
35 - containerPort: 8080
36
37 - name: gluetun
38 image: qmcgaw/gluetun
39 env:
40 - name: VPNSP
41 value: "protonvpn"
42 - name: OPENVPN_USER
43 valueFrom:
44 secretKeyRef:
45 name: protonvpn-secrets
46 key: PROTONVPN_USER
47 - name: OPENVPN_PASSWORD
48 valueFrom:
49 secretKeyRef:
50 name: protonvpn-secrets
51 key: PROTONVPN_PASSWORD
52 - name: COUNTRY
53 value: "Germany"
54 securityContext:
55 capabilities:
56 add:
57 - NET_ADMIN
58 volumeMounts:
59 - name: gluetun-config
60 mountPath: /gluetun
61
62 volumes:
63 - name: config
64 persistentVolumeClaim:
65 claimName: qbitt-config
66 - name: downloads
67 persistentVolumeClaim:
68 claimName: qbitt-download
69 - name: gluetun-config
70 persistentVolumeClaim:
71 claimName: gluetun-config
Creating ClusterIP Services#
For our media server applications to communicate efficiently within the Kubernetes cluster without exposing them directly to the external network, we utilize ClusterIP services.
To set this up, we create a app-service.yaml for each application (taking Radarr as an example here):
create app-service.yaml
1apiVersion: v1
2kind: Service
3metadata:
4 name: app #radarr for example
5 namespace: media
6spec:
7 type: ClusterIP
8 ports:
9 - port: 80
10 targetPort: 7878
11 selector:
12 app: app #radarr for example
kubectl apply -f app-service.yaml
Creating middleware for Traefik#
For enhanced security and to ensure smooth functioning with Traefik, we define middleware:
- The middleware, named
default-headers-media
, is configured in themedia
namespace. - It sets various security headers, including XSS protection and options to prevent MIME sniffing, among others.
Create default-headers-media.yaml
1apiVersion: traefik.containo.us/v1alpha1
2kind: Middleware
3metadata:
4 name: default-headers-media
5 namespace: media
6spec:
7 headers:
8 browserXssFilter: true
9 contentTypeNosniff: true
10 forceSTSHeader: true
11 stsIncludeSubdomains: true
12 stsPreload: true
13 stsSeconds: 15552000
14 customFrameOptionsValue: SAMEORIGIN
15 customRequestHeaders:
16 X-Forwarded-Proto: https
Apply with:
kubectl apply -f default-headers-media.yaml
Creating Ingress Route for Each Application#
To expose each application securely, we create IngressRoutes using Traefik:
- An IngressRoute for the application (such as Radarr) is defined, which uses the
traefik-external
ingress class. - It listens on the
websecure
entry point and routes traffic based on the host (movies.merox.cloud
in this example, replace with your domain). - The middleware
default-headers-media
is applied to enhance security. - TLS configuration is included, referencing a secret that contains the SSL/TLS certificate.
Create app-ingress-route.yaml
1apiVersion: traefik.containo.us/v1alpha1
2kind: IngressRoute
3metadata:
4 name: app #radarr for example
5 namespace: media
6 annotations:
7 kubernetes.io/ingress.class: traefik-external
8spec:
9 entryPoints:
10 - websecure
11 routes:
12 - match: Host(`movies.merox.cloud`) # change to your domain
13 kind: Rule
14 services:
15 - name: app #radarr for example
16 port: 80
17 - match: Host(`movies.merox.cloud`) # change to your domain
18 kind: Rule
19 services:
20 - name: app #radarr for example
21 port: 80
22 middlewares:
23 - name: default-headers-media
24 tls:
25 secretName: mycert-tls # change to your cert name
Apply with:
kubectl apply -f app-ingress-route.yaml
Q&A#
This concludes the necessary steps and configurations to deploy a resilient media server in a Kubernetes cluster successfully.
Manifest files#
Just copy and deploy all you need in no time