0352b7d556a9d9cd6c6c453ed7ad0fe786bd55fb
Media Stack Kubernetes Deployment
A complete self-hosted media solution deployed on Kubernetes.
Stack Components
| Component | Purpose | Port | Image |
|---|---|---|---|
| Jellyfin | Media server + Live TV frontend | 8096 | jellyfin/jellyfin:10.11.5 |
| Sonarr | TV show management | 8989 | lscr.io/linuxserver/sonarr:latest |
| Radarr | Movie management | 7878 | lscr.io/linuxserver/radarr:latest |
| Lidarr | Music management | 8686 | lscr.io/linuxserver/lidarr:latest |
| Prowlarr | Indexer management | 9696 | lscr.io/linuxserver/prowlarr:latest |
| qBittorrent | Download client | 8080 | lscr.io/linuxserver/qbittorrent:latest |
| Dispatcharr | IPTV/M3U proxy for live TV | 9191 | ghcr.io/dispatcharr/dispatcharr:latest |
Network Configuration
- Network:
10.0.0.0/24 - K8s Control Plane:
10.0.0.69(k8scontrol) - K8s Workers:
10.0.0.70-73(k8sworker1-3) - NFS Server:
10.0.0.230 - Kubernetes Version: 1.35
NFS Share Structure
Your NFS server (10.0.0.230) should have these directories:
/srv/nfs/media/
├── config/
│ ├── jellyfin/
│ ├── sonarr/
│ ├── radarr/
│ ├── lidarr/
│ ├── prowlarr/
│ ├── qbittorrent/
│ └── dispatcharr/
├── downloads/
│ ├── complete/
│ └── incomplete/
├── media/
│ ├── movies/
│ ├── tv/
│ └── music/
└── transcode/
Pre-requisites
1. Configure NFS Server (10.0.0.230)
# On NFS server
sudo mkdir -p /srv/nfs/media/{config/{jellyfin,sonarr,radarr,lidarr,prowlarr,qbittorrent,dispatcharr},downloads/{complete,incomplete},media/{movies,tv,music},transcode}
# Set permissions (adjust UID/GID as needed - 1000:1000 is common)
sudo chown -R 1000:1000 /srv/nfs/media
# Add to /etc/exports
echo "/srv/nfs/media 10.0.0.0/24(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
# Apply exports
sudo exportfs -ra
2. Install NFS Client on K8s Nodes
# On each worker node (k8sworker1-3)
sudo apt-get update && sudo apt-get install -y nfs-common
3. Verify NFS Connectivity
# From any worker node
showmount -e 10.0.0.230
Deployment
Manual Deployment (in order)
Without VPN:
# Create namespace
kubectl apply -f base/namespace.yaml
# Create storage
kubectl apply -f base/nfs-storage.yaml
# Create config
kubectl apply -f base/configmap.yaml
# Create PVCs
kubectl apply -f base/pvcs.yaml
# Deploy applications
kubectl apply -f base/jellyfin.yaml
kubectl apply -f base/sonarr.yaml
kubectl apply -f base/radarr.yaml
kubectl apply -f base/lidarr.yaml
kubectl apply -f base/prowlarr.yaml
kubectl apply -f base/qbittorrent.yaml
kubectl apply -f base/dispatcharr.yaml
With VPN (after configuring VPN credentials in base/vpn/mullvad-secret.yaml):
Download a .conf file from your Mullvad account and update the private key and wireguard address in base/vpn/mullvad-secret.yaml)
# Create namespace
kubectl apply -f base/namespace.yaml
# Create storage
kubectl apply -f base/nfs-storage.yaml
# Create config
kubectl apply -f base/configmap.yaml
# Create VPN-specific resources
kubectl apply -f base/vpn/gluetun-config.yaml
kubectl apply -f base/vpn/qbittorrent-init-configmap.yaml
kubectl apply -f base/vpn/mullvad-secret.yaml
# Create PVCs
kubectl apply -f base/pvcs.yaml
# Deploy non-VPN applications
kubectl apply -f base/jellyfin.yaml
kubectl apply -f base/sonarr.yaml
kubectl apply -f base/radarr.yaml
kubectl apply -f base/lidarr.yaml
# Deploy VPN-enabled applications
kubectl apply -f base/vpn/prowlarr-vpn.yaml
kubectl apply -f base/vpn/qbittorrent-vpn.yaml
kubectl apply -f base/vpn/dispatcharr-vpn.yaml
Automated deployment
A deploy.sh script can be used as such:
Without VPN:
./deploy.sh
With VPN (recommended for qBittorrent, Prowlarr, Dispatcharr):
./deploy.sh --vpn
Optionally to change the VPN location amend SERVER_CITIES in the base/vpn/gluetun-config.yaml file.
To enable port forwarding:
- Go to your mullvad.net account and navigate to
account/ports - Add a port for your wireguard key
- Amend the
FIREWALL_VPN_INPUT_PORTSvalue to your port in thebase/vpn/gluetun-config.yamlfile - Configure qBittorrent to point to your client
Accessing Services
After deployment, services are available via NodePort:
| Service | URL |
|---|---|
| Jellyfin | http://10.0.0.70:30096 |
| Sonarr | http://10.0.0.70:30989 |
| Radarr | http://10.0.0.70:30878 |
| Lidarr | http://10.0.0.70:30686 |
| Prowlarr | http://10.0.0.70:30696 |
| qBittorrent | http://10.0.0.70:30080 |
| Dispatcharr | http://10.0.0.70:30191 |
Replace 10.0.0.70 with any worker node IP
Post-Deployment Configuration
1. qBittorrent
- Check container logs for temporary password:
kubectl logs -n media deployment/qbittorrent - Login and change password immediately
- Set download paths to
/downloads/completeand/downloads/incomplete
2. Prowlarr
- Add your indexers
- Connect to Sonarr, Radarr, and Lidarr via Settings → Apps
- Use internal service names:
http://sonarr:8989,http://radarr:7878,http://lidarr:8686
3. Sonarr/Radarr/Lidarr
- Add qBittorrent as download client:
http://qbittorrent:8080 - Set media library paths:
/media/tv,/media/movies,/media/music - Configure quality profiles and root folders
4. Jellyfin
- Run initial setup wizard
- Add media libraries pointing to
/media/movies,/media/tv,/media/music - For Live TV: Settings → Live TV → Add Tuner Device → HDHomeRun
- Use Dispatcharr's HDHomeRun URL:
http://dispatcharr:9191
- Use Dispatcharr's HDHomeRun URL:
5. Dispatcharr
- Add your M3U playlist sources
- Configure EPG sources
- Enable channels you want to watch
Useful Commands
# Restart a deployment
kubectl rollout restart deployment/jellyfin -n media
# Scale down all (for maintenance)
kubectl scale deployment --all -n media --replicas=0
# Scale back up
kubectl scale deployment --all -n media --replicas=1
Troubleshooting
Permission Denied Errors
- Ensure PUID/PGID in configmap matches NFS share ownership
- Check that
no_root_squashis set in NFS exports
Upgrading
To upgrade images:
# Update a single deployment
kubectl set image deployment/jellyfin -n media jellyfin=jellyfin/jellyfin:NEW_VERSION
# Or edit the YAML and reapply
kubectl apply -f base/jellyfin.yaml
Cleanup
# Delete everything
kubectl delete namespace media
# This will remove all deployments, services, and PVCs
# PVs and NFS data will remain
Description