arrstack: sonarr/radarr/prowlarr/nzbget on shared CephFS media (#366)

## Why
Stands up the download-automation stack (sonarr, radarr, prowlarr, nzbget) in a new `arrstack` namespace, sharing the same CephFS TV/movies subvolumes that jellyfin serves read-only (jellyfin static-PV rework is on the base branch, PR #237). Downloads and libraries live on one filesystem per media type so the arrs import with atomic hardlink moves.

## Changes
- Add `apps/base/arrstack/` namespace with its own static CephFS PVs (`arrstack-media-tv`, `arrstack-media-movies`) + RWX PVCs (`media-tv`, `media-movies`) bound to the same rootPaths jellyfin uses; each namespace gets a unique PV name/volumeHandle pinned by claimRef.
- Deploy sonarr (mounts media-tv RW), radarr (media-movies RW), prowlarr (config only), nzbget (both media PVCs RW); single replica, Recreate strategy, LinuxServer images via artifactapi dockerhub remote, PUID/PGID/TZ, probes, fsGroup for CephFS writability.
- Per-app config PVCs on `cephrbd-fast-retain` (RWO, 5Gi) for the SQLite state; internal-only Gateway + HTTPRoute per app (traefik-internal, vault-issuer, external-dns `<app>.k8s.syd1.au.unkin.net`), no oauth yet.
- nzbget: initContainer seeds `/config/nzbget.conf` (copy image template if absent, append managed path/category block once) and creates the media dirs; category `tv` -> `/media/tv/downloads`, `movies` -> `/media/movies/downloads`.
- Wire ArgoCD: add `apps/overlays/*/arrstack` to the media ApplicationSet. (media AppProject already gains the arrstack destination + PersistentVolume whitelist on the base branch.)

## Depends on
terraform-artifactapi PR #31 (allowlist `^linuxserver/` on the dockerhub remote) must be merged + applied before these pods can pull.

## Follow-up (terraform)
Root folders (/media/tv/series, /media/movies/films), download-client wiring (host `nzbget`, port 6789, categories tv/movies) and API keys are configured later via terraform-arr.

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Co-authored-by: Ben Vin <neotheo@gmail.com>
Reviewed-on: #366
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #366.
This commit is contained in:
2026-08-15 16:22:06 +10:00
committed by BenVincent
parent 40ba1730de
commit fc08f1fecc
32 changed files with 990 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sonarr
namespace: arrstack
spec:
replicas: 1
strategy:
# RWO config PVC + single stateful SQLite DB: never run two pods at once.
type: Recreate
selector:
matchLabels:
app: sonarr
template:
metadata:
labels:
app: sonarr
spec:
securityContext:
# LinuxServer images init as root via s6 then step down to PUID/PGID.
# fsGroup makes the shared CephFS group-writable for that user;
# OnRootMismatch avoids a recursive chown of the whole media tree.
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
containers:
- name: sonarr
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/sonarr:4.0.19
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8989
protocol: TCP
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: Australia/Sydney
livenessProbe:
httpGet:
path: /ping
port: http
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumeMounts:
- name: config
mountPath: /config
- name: media-tv
mountPath: /media/tv
volumes:
- name: config
persistentVolumeClaim:
claimName: sonarr-config
- name: media-tv
persistentVolumeClaim:
claimName: media-tv
+37
View File
@@ -0,0 +1,37 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: sonarr.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: sonarr.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: sonarr
namespace: arrstack
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: sonarr.k8s.syd1.au.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: sonarr.k8s.syd1.au.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: sonarr-tls
mode: Terminate
+49
View File
@@ -0,0 +1,49 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: sonarr-http-redirect
namespace: arrstack
spec:
hostnames:
- sonarr.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: sonarr
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: sonarr-route
namespace: arrstack
spec:
hostnames:
- sonarr.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: sonarr
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: sonarr
port: 8989
weight: 1
matches:
- path:
type: PathPrefix
value: /
@@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pvc-config.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
+16
View File
@@ -0,0 +1,16 @@
---
# Sonarr config + SQLite DB. RWO on cephrbd (block) — the arr apps' SQLite
# does not tolerate CephFS locking. Retain: this is state.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarr-config
namespace: arrstack
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: cephrbd-fast-retain
volumeMode: Filesystem
+15
View File
@@ -0,0 +1,15 @@
---
apiVersion: v1
kind: Service
metadata:
name: sonarr
namespace: arrstack
spec:
ports:
- name: http
port: 8989
protocol: TCP
targetPort: http
selector:
app: sonarr
type: ClusterIP