From ba991117539f4e6c20a6751ef32edd7435824161 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Fri, 10 Apr 2026 22:50:13 -0500 Subject: [PATCH] Add media stack: Plex, Radarr, Sonarr, Prowlarr, qBittorrent, Bazarr, Overseerr - Namespace: media (created via Terraform) - Storage: hostPath PV at /media on Legion (movies, tv/shows, tv/anime, downloads) - VPN: gluetun sidecar with ProtonVPN WireGuard (US-TX#253), all torrent traffic tunneled - Radarr: movie automation with /media/movies root - Sonarr: TV/anime automation with /media/tv/{shows,anime} roots - Prowlarr: indexer aggregator (YTS, 1337x, Nyaa for anime) - qBittorrent: torrent client, network namespaced behind gluetun VPN - Bazarr: automatic subtitle downloads (connects to Radarr + Sonarr) - Overseerr: family request portal at watch.nook.family - Plex: media server at plex.nook.family --- servers/legion/apps/media.yaml | 20 +++ servers/legion/k8s/media/bazarr.yaml | 77 +++++++++++ .../legion/k8s/media/external-secrets.yaml | 25 ++++ .../legion/k8s/media/gluetun-qbittorrent.yaml | 115 ++++++++++++++++ servers/legion/k8s/media/ingress.yaml | 58 ++++++++ servers/legion/k8s/media/overseerr.yaml | 75 +++++++++++ servers/legion/k8s/media/plex.yaml | 103 +++++++++++++++ servers/legion/k8s/media/prowlarr.yaml | 74 +++++++++++ servers/legion/k8s/media/pvc.yaml | 124 ++++++++++++++++++ servers/legion/k8s/media/radarr.yaml | 80 +++++++++++ servers/legion/k8s/media/sonarr.yaml | 81 ++++++++++++ servers/legion/media.tf | 13 ++ 12 files changed, 845 insertions(+) create mode 100644 servers/legion/apps/media.yaml create mode 100644 servers/legion/k8s/media/bazarr.yaml create mode 100644 servers/legion/k8s/media/external-secrets.yaml create mode 100644 servers/legion/k8s/media/gluetun-qbittorrent.yaml create mode 100644 servers/legion/k8s/media/ingress.yaml create mode 100644 servers/legion/k8s/media/overseerr.yaml create mode 100644 servers/legion/k8s/media/plex.yaml create mode 100644 servers/legion/k8s/media/prowlarr.yaml create mode 100644 servers/legion/k8s/media/pvc.yaml create mode 100644 servers/legion/k8s/media/radarr.yaml create mode 100644 servers/legion/k8s/media/sonarr.yaml create mode 100644 servers/legion/media.tf diff --git a/servers/legion/apps/media.yaml b/servers/legion/apps/media.yaml new file mode 100644 index 0000000..3fe01bb --- /dev/null +++ b/servers/legion/apps/media.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: media + namespace: argocd +spec: + project: default + source: + repoURL: https://git.neuralplatform.ai/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/media + destination: + server: https://kubernetes.default.svc + namespace: media + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/k8s/media/bazarr.yaml b/servers/legion/k8s/media/bazarr.yaml new file mode 100644 index 0000000..c148d2f --- /dev/null +++ b/servers/legion/k8s/media/bazarr.yaml @@ -0,0 +1,77 @@ +# Bazarr — automatic subtitle downloader +# Connects to Radarr + Sonarr, downloads subtitles from OpenSubtitles/Subscene/etc. +# After deploy, configure at bazarr:6767: +# 1. Settings > Radarr: URL=http://radarr:7878, API key from Radarr Settings > General +# 2. Settings > Sonarr: URL=http://sonarr:8989, API key from Sonarr Settings > General +# 3. Settings > Providers: add OpenSubtitles.com (free account) for best coverage +# 4. Settings > Languages: add English (and any other desired languages) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bazarr + namespace: media + labels: + app: bazarr +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: bazarr + template: + metadata: + labels: + app: bazarr + spec: + containers: + - name: bazarr + image: lscr.io/linuxserver/bazarr:latest + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "America/Chicago" + ports: + - containerPort: 6767 + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + resources: + requests: + memory: 128Mi + cpu: 50m + limits: + memory: 512Mi + cpu: 200m + livenessProbe: + httpGet: + path: / + port: 6767 + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: config + persistentVolumeClaim: + claimName: bazarr-config + - name: media + persistentVolumeClaim: + claimName: media-data +--- +apiVersion: v1 +kind: Service +metadata: + name: bazarr + namespace: media +spec: + selector: + app: bazarr + ports: + - name: http + port: 6767 + targetPort: 6767 + type: ClusterIP diff --git a/servers/legion/k8s/media/external-secrets.yaml b/servers/legion/k8s/media/external-secrets.yaml new file mode 100644 index 0000000..1d28f57 --- /dev/null +++ b/servers/legion/k8s/media/external-secrets.yaml @@ -0,0 +1,25 @@ +# media-secrets — ProtonVPN WireGuard key + Plex claim token +# PLEX_CLAIM: run `vault kv patch secret/plex claim_token=` right before first deploy +# Get token from: https://www.plex.tv/claim (expires in 4 minutes) +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: media-secrets + namespace: media +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: media-secrets + creationPolicy: Owner + data: + - secretKey: PROTONVPN_PRIVATE_KEY + remoteRef: + key: secret/data/protonvpn + property: private_key + - secretKey: PLEX_CLAIM + remoteRef: + key: secret/data/plex + property: claim_token diff --git a/servers/legion/k8s/media/gluetun-qbittorrent.yaml b/servers/legion/k8s/media/gluetun-qbittorrent.yaml new file mode 100644 index 0000000..a0c0e59 --- /dev/null +++ b/servers/legion/k8s/media/gluetun-qbittorrent.yaml @@ -0,0 +1,115 @@ +# gluetun + qBittorrent — all torrent traffic routed through ProtonVPN WireGuard +# gluetun runs as the VPN container; qBittorrent shares its network namespace (same pod) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: qbittorrent + namespace: media + labels: + app: qbittorrent +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: qbittorrent + template: + metadata: + labels: + app: qbittorrent + spec: + initContainers: + # Ensure /dev/net/tun exists on the node + - name: tun-setup + image: busybox:latest + command: ["sh", "-c", "mkdir -p /dev/net && [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200 && chmod 666 /dev/net/tun"] + securityContext: + privileged: true + containers: + # VPN container — must come first so network is up before qBittorrent starts + - name: gluetun + image: ghcr.io/qdm12/gluetun:latest + securityContext: + capabilities: + add: ["NET_ADMIN"] + env: + - name: VPN_SERVICE_PROVIDER + value: "custom" + - name: VPN_TYPE + value: "wireguard" + - name: WIREGUARD_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: media-secrets + key: PROTONVPN_PRIVATE_KEY + - name: WIREGUARD_PUBLIC_KEY + value: "mngiSxBpH7GU24nnWdBEcnhDnCPn2jq5+ZP3zwPwISA=" + - name: WIREGUARD_ADDRESSES + value: "10.2.0.2/32" + - name: VPN_ENDPOINT_IP + value: "95.173.217.29" + - name: VPN_ENDPOINT_PORT + value: "51820" + # Allow cluster-internal traffic to bypass VPN (for Radarr/Sonarr → qBittorrent API) + - name: FIREWALL_OUTBOUND_SUBNETS + value: "10.42.0.0/16,10.43.0.0/16" + - name: UPDATER_PERIOD + value: "0" + ports: + - containerPort: 8888 # gluetun HTTP proxy (unused but required) + resources: + requests: + memory: 64Mi + cpu: 50m + limits: + memory: 128Mi + cpu: 200m + + # qBittorrent — shares gluetun's network namespace, all traffic through VPN + - name: qbittorrent + image: lscr.io/linuxserver/qbittorrent:latest + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "America/Chicago" + - name: WEBUI_PORT + value: "8080" + ports: + - containerPort: 8080 + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: 500m + volumes: + - name: config + persistentVolumeClaim: + claimName: qbittorrent-config + - name: media + persistentVolumeClaim: + claimName: media-data +--- +apiVersion: v1 +kind: Service +metadata: + name: qbittorrent + namespace: media +spec: + selector: + app: qbittorrent + ports: + - name: webui + port: 8080 + targetPort: 8080 + type: ClusterIP diff --git a/servers/legion/k8s/media/ingress.yaml b/servers/legion/k8s/media/ingress.yaml new file mode 100644 index 0000000..108d072 --- /dev/null +++ b/servers/legion/k8s/media/ingress.yaml @@ -0,0 +1,58 @@ +# watch.nook.family → Overseerr (family request portal) +# NOTE: After deploying, add this route in Cloudflare Zero Trust: +# Zero Trust > Networks > Tunnels > neural-platform > Public Hostname +# Hostname: watch.nook.family | Service: http://traefik.kube-system:80 +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: overseerr + namespace: media + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - watch.nook.family + secretName: overseerr-tls + rules: + - host: watch.nook.family + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: overseerr + port: + number: 5055 +--- +# plex.nook.family → Plex +# NOTE: Also add in Cloudflare Zero Trust: +# Hostname: plex.nook.family | Service: http://traefik.kube-system:80 +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: plex + namespace: media + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - plex.nook.family + secretName: plex-tls + rules: + - host: plex.nook.family + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: plex + port: + number: 32400 diff --git a/servers/legion/k8s/media/overseerr.yaml b/servers/legion/k8s/media/overseerr.yaml new file mode 100644 index 0000000..963a565 --- /dev/null +++ b/servers/legion/k8s/media/overseerr.yaml @@ -0,0 +1,75 @@ +# Overseerr — family request portal (watch.nook.family) +# Family members browse and request content here +# After deploy, complete setup wizard at watch.nook.family: +# 1. Sign in with Plex account +# 2. Connect to Plex server +# 3. Add Radarr (http://radarr:7878) and Sonarr (http://sonarr:8989) +# 4. Create managed user accounts for each family member +# 5. Set request permissions per user (kids require approval) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: overseerr + namespace: media + labels: + app: overseerr +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: overseerr + template: + metadata: + labels: + app: overseerr + spec: + containers: + - name: overseerr + image: sctx/overseerr:latest + env: + - name: TZ + value: "America/Chicago" + ports: + - containerPort: 5055 + volumeMounts: + - name: config + mountPath: /app/config + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: 500m + livenessProbe: + httpGet: + path: /api/v1/status + port: 5055 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /api/v1/status + port: 5055 + initialDelaySeconds: 15 + periodSeconds: 10 + volumes: + - name: config + persistentVolumeClaim: + claimName: overseerr-config +--- +apiVersion: v1 +kind: Service +metadata: + name: overseerr + namespace: media +spec: + selector: + app: overseerr + ports: + - name: http + port: 5055 + targetPort: 5055 + type: ClusterIP diff --git a/servers/legion/k8s/media/plex.yaml b/servers/legion/k8s/media/plex.yaml new file mode 100644 index 0000000..8952106 --- /dev/null +++ b/servers/legion/k8s/media/plex.yaml @@ -0,0 +1,103 @@ +# Plex Media Server +# BEFORE FIRST DEPLOY: add claim token to Vault +# 1. Go to https://www.plex.tv/claim (token expires in 4 minutes) +# 2. vault kv put secret/plex claim_token= +# 3. Then push and let Argo CD deploy +# After first startup the token is stored in config and can be left as-is +# +# Post-deploy setup: +# 1. Access via plex.nook.family or port-forward :32400 +# 2. Add libraries: +# - Movies → /media/movies +# - TV Shows → /media/tv/shows +# - Anime (TV Shows) → /media/tv/anime +# 3. Settings > Manage > Users & Sharing → create Plex Home managed users +# 4. Set content rating limits per child profile (G/PG/PG-13) +# 5. Enable PIN on adult profiles +apiVersion: apps/v1 +kind: Deployment +metadata: + name: plex + namespace: media + labels: + app: plex +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: plex + template: + metadata: + labels: + app: plex + spec: + containers: + - name: plex + image: lscr.io/linuxserver/plex:latest + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "America/Chicago" + - name: VERSION + value: "docker" + - name: PLEX_CLAIM + valueFrom: + secretKeyRef: + name: media-secrets + key: PLEX_CLAIM + optional: true # safe after first boot + - name: ADVERTISE_IP + value: "https://plex.nook.family:443" + ports: + - containerPort: 32400 + name: plex + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + resources: + requests: + memory: 1Gi + cpu: 500m + limits: + memory: 4Gi + cpu: 2000m + livenessProbe: + httpGet: + path: /identity + port: 32400 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /identity + port: 32400 + initialDelaySeconds: 30 + periodSeconds: 10 + volumes: + - name: config + persistentVolumeClaim: + claimName: plex-config + - name: media + persistentVolumeClaim: + claimName: media-data +--- +apiVersion: v1 +kind: Service +metadata: + name: plex + namespace: media +spec: + selector: + app: plex + ports: + - name: plex + port: 32400 + targetPort: 32400 + type: ClusterIP diff --git a/servers/legion/k8s/media/prowlarr.yaml b/servers/legion/k8s/media/prowlarr.yaml new file mode 100644 index 0000000..c509805 --- /dev/null +++ b/servers/legion/k8s/media/prowlarr.yaml @@ -0,0 +1,74 @@ +# Prowlarr — indexer aggregator (connects to torrent trackers, feeds Radarr + Sonarr) +# After deploy, add indexers at prowlarr:9696: +# Recommended public indexers for movies/TV/anime: +# - YTS (movies — excellent quality, very reliable) +# - 1337x (general) +# - TorrentGalaxy (general) +# - Nyaa.si (anime — essential for anime/manga) +# - AnimeTosho (anime with good subtitle/dub tagging) +# Then sync to Radarr + Sonarr via Settings > Apps +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prowlarr + namespace: media + labels: + app: prowlarr +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: prowlarr + template: + metadata: + labels: + app: prowlarr + spec: + containers: + - name: prowlarr + image: lscr.io/linuxserver/prowlarr:latest + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "America/Chicago" + ports: + - containerPort: 9696 + volumeMounts: + - name: config + mountPath: /config + resources: + requests: + memory: 128Mi + cpu: 50m + limits: + memory: 512Mi + cpu: 300m + livenessProbe: + httpGet: + path: /ping + port: 9696 + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: config + persistentVolumeClaim: + claimName: prowlarr-config +--- +apiVersion: v1 +kind: Service +metadata: + name: prowlarr + namespace: media +spec: + selector: + app: prowlarr + ports: + - name: http + port: 9696 + targetPort: 9696 + type: ClusterIP diff --git a/servers/legion/k8s/media/pvc.yaml b/servers/legion/k8s/media/pvc.yaml new file mode 100644 index 0000000..b32c17d --- /dev/null +++ b/servers/legion/k8s/media/pvc.yaml @@ -0,0 +1,124 @@ +# Shared media PV — hostPath /media on Legion (single-node cluster) +apiVersion: v1 +kind: PersistentVolume +metadata: + name: media-data +spec: + capacity: + storage: 500Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + hostPath: + path: /media + type: Directory + claimRef: + namespace: media + name: media-data +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: media-data + namespace: media +spec: + storageClassName: "" + volumeName: media-data + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 500Gi +--- +# Per-service config PVCs (small, local-path provisioner) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: plex-config + namespace: media +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: radarr-config + namespace: media +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sonarr-config + namespace: media +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: prowlarr-config + namespace: media +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: qbittorrent-config + namespace: media +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: bazarr-config + namespace: media +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: overseerr-config + namespace: media +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/servers/legion/k8s/media/radarr.yaml b/servers/legion/k8s/media/radarr.yaml new file mode 100644 index 0000000..638e174 --- /dev/null +++ b/servers/legion/k8s/media/radarr.yaml @@ -0,0 +1,80 @@ +# Radarr — movie automation (monitors new releases, grabs torrents via Prowlarr/qBittorrent) +# After deploy, configure at http://radarr.media.svc:7878 or via kubectl port-forward +# Root folder: /media/movies | Download client: qbittorrent:8080 +# Quality profile: set MPAA filter to allow G/PG/PG-13/R/NR (unrated for documentaries) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: radarr + namespace: media + labels: + app: radarr +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: radarr + template: + metadata: + labels: + app: radarr + spec: + containers: + - name: radarr + image: lscr.io/linuxserver/radarr:latest + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "America/Chicago" + ports: + - containerPort: 7878 + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: 500m + livenessProbe: + httpGet: + path: /ping + port: 7878 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /ping + port: 7878 + initialDelaySeconds: 15 + periodSeconds: 10 + volumes: + - name: config + persistentVolumeClaim: + claimName: radarr-config + - name: media + persistentVolumeClaim: + claimName: media-data +--- +apiVersion: v1 +kind: Service +metadata: + name: radarr + namespace: media +spec: + selector: + app: radarr + ports: + - name: http + port: 7878 + targetPort: 7878 + type: ClusterIP diff --git a/servers/legion/k8s/media/sonarr.yaml b/servers/legion/k8s/media/sonarr.yaml new file mode 100644 index 0000000..ad01698 --- /dev/null +++ b/servers/legion/k8s/media/sonarr.yaml @@ -0,0 +1,81 @@ +# Sonarr — TV show & anime automation +# After deploy, configure at sonarr:8989 +# Root folders: /media/tv/shows and /media/tv/anime +# For anime: add custom format preferring "Dual Audio" / "English Dub" releases +# Download client: qbittorrent:8080 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sonarr + namespace: media + labels: + app: sonarr +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: sonarr + template: + metadata: + labels: + app: sonarr + spec: + containers: + - name: sonarr + image: lscr.io/linuxserver/sonarr:latest + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "America/Chicago" + ports: + - containerPort: 8989 + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: 500m + livenessProbe: + httpGet: + path: /ping + port: 8989 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /ping + port: 8989 + initialDelaySeconds: 15 + periodSeconds: 10 + volumes: + - name: config + persistentVolumeClaim: + claimName: sonarr-config + - name: media + persistentVolumeClaim: + claimName: media-data +--- +apiVersion: v1 +kind: Service +metadata: + name: sonarr + namespace: media +spec: + selector: + app: sonarr + ports: + - name: http + port: 8989 + targetPort: 8989 + type: ClusterIP diff --git a/servers/legion/media.tf b/servers/legion/media.tf new file mode 100644 index 0000000..23704f5 --- /dev/null +++ b/servers/legion/media.tf @@ -0,0 +1,13 @@ +# Media stack — Plex, Radarr, Sonarr, Prowlarr, qBittorrent, Bazarr, Overseerr +# Namespace only — all resources managed by Argo CD +# See: apps/media.yaml, k8s/media/ + +resource "kubernetes_namespace" "media" { + metadata { + name = "media" + labels = { + managed-by = "terraform" + tier = "apps" + } + } +}