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

Merged
benvin merged 12 commits from benvin/media-arr-apps into main 2026-08-15 16:22:07 +10:00
Member

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.

## 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.
benvin changed target branch from benvin/jellyfin-ha to main 2026-08-15 13:39:01 +10:00
benvin added 11 commits 2026-08-15 13:39:01 +10:00
Deploys the jellyfin-ha fork (git.unkin.net/unkin/jellyfin-ha) to au-syd1
via ArgoCD, under a dedicated media AppProject/ApplicationSet rather than
extending platform.

- New media AppProject + media-apps ApplicationSet (watches
  apps/overlays/*/jellyfin); registered in the argocd kustomizations
- apps/base/jellyfin: namespace, deployment (single replica to start),
  service, in-namespace Redis (transcode session store), gateway + httproute
  at jellyfin.k8s.syd1.au.unkin.net
- Storage: RWO config (cephrbd), RWX transcode scratch and RWX media
  library (cephfs) per the HA fork's pod-takeover requirement
- au-syd1 overlay
Rework jellyfin-ha into a true-HA StatefulSet deployment
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
f89e1c8260
Turn the single-replica jellyfin-ha app into a proper high-availability
deployment so the fork's Redis-coordinated distributed transcoding and
PostgreSQL main database can actually be exercised.

- Replace the Deployment with a 2-replica StatefulSet for stable pod
  identity; set JELLYFIN_INSTANCE_ID from metadata.name (the fork's Redis
  transcode-lease owner id), add soft podAntiAffinity and a PDB
  minAvailable 1.
- Move the main Jellyfin DB to PostgreSQL via a CloudNativePG trio
  (3-instance Cluster, PgBouncer Pooler, Ceph RGW barman backups) mirroring
  the litellm pattern; an init container writes database.xml selecting the
  fork's Jellyfin-PostgreSQL provider and the DSN is composed from the
  CNPG-generated app secret pointed at the pooler.
- Share /config on an RWX cephfs PVC across replicas; keep /cache per-pod
  via a volumeClaimTemplate.
- Fix the transcode mount to the fork's real path /config/transcodes on the
  RWX PVC (raid5) so a surviving pod can resume the segments of the pod it
  takes over.
- Add Intel iGPU hardware transcoding via the gpu.intel.com/i915 device
  plugin resource plus render/video supplemental groups.
- Switch the Service to sessionAffinity ClientIP to reduce transcode churn.
- Disable UDP auto-discovery. Library scans still run on every replica; a
  single-scanner leader election is a planned follow-up.
Enable Intel iGPU hardware transcode by default for jellyfin-ha
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
ceb2114467
The pod already requests gpu.intel.com/i915 and joins the render/video
groups, but Jellyfin transcodes in software until hardware acceleration
is turned on in its encoding config, which the fork does not template.

Seed /config/config/encoding.xml from the init container so VAAPI on the
injected Intel render node (/dev/dri/renderD128) is active on first boot:
HardwareAccelerationType vaapi, EnableHardwareEncoding, h264/hevc decode,
tonemapping left off. Rename the init container to inject-config and write
each seed file only when absent, so admin changes persisted to the shared
RWX /config are never clobbered on restart.
jellyfin-ha: pull image from artifactapi docker-internal registry
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
648ff16940
The build repo's release pipeline pushes the runtime image to the
artifactapi local docker registry, not Gitea. Point the StatefulSet at
artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.1.0 so
the deploy pulls the image the build actually produces. Pull is anonymous
in-cluster (no imagePullSecret), matching every other docker-internal
workload in the estate (encapi, pdbmux, bind-operator, cephrgw, age-api).
jellyfin: split media PVC into tv/movies on cephfs-raid5-delete
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
6769b5291e
Separate tv and movies onto their own volumes so sonarr/radarr can each
mount and manage their library individually later, and move media storage
off cephfs-raid6-retain onto cephfs-raid5-delete.

- Remove single jellyfin-media PVC (RWX, cephfs-raid6-retain)
- Add jellyfin-media-tv and jellyfin-media-movies PVCs (RWX, cephfs-raid5-delete, 500Gi each, expandable)
- Mount tv read-only at /media/tv and movies read-only at /media/movies in the statefulset
- Update kustomization resources to reference the two new PVCs
Give the jellyfin backup user a second bucket so k8up can store restic
backups of the jellyfin-config PVC alongside the existing CNPG barman
bucket (one user, two buckets).

- Add a jellyfin-config-backup Bucket owned by the existing
  cnpg-jellyfin-backup ObjectStoreUser
- Add a read-write BucketAccess emitting jellyfin-config-backup-s3 with
  the S3 creds k8up consumes
- Wire the new file into the jellyfin kustomization
jellyfin: back up the config PVC with a k8up Schedule
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
57ac95bdec
Protect the jellyfin-config PVC (library metadata, plugins, config XML)
with daily restic backups to the new Ceph RGW config-backup bucket.

- Add a k8up.io Schedule: daily backup (02:00), weekly prune (Sun 03:00,
  keep 14 daily/8 weekly/12 monthly) and weekly check (Sun 04:00)
- Source S3 creds from the cephrgw BucketAccess Secret and the restic
  repo password from Vault via a VaultStaticSecret (kv path
  kubernetes/namespace/jellyfin/default/k8up-restic)
- Add the namespace VaultAuth (default role/SA) VSO needs to sync it
- Mount the reflected vault-ca-cert into the restic pods so restic trusts
  the internal unkin.net CA on s3.ceph.unkin.net
- Wire the new files into the jellyfin kustomization
jellyfin: scope k8up backup to config PVC only
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
066dc41b55
The jellyfin-config Schedule had no PVC selector and the k8up chart runs
skipWithoutAnnotation=false, so restic swept in every PVC in the namespace
(media, transcode scratch, redis, per-pod cache, CNPG data) — RWO volumes
also fail to mount while in use. k8up 4.10.0's Schedule CRD has no positive
PVC selector, so exclude every non-config PVC by annotation.

- Annotate media-tv, media-movies, transcode and redis-data PVCs with
  k8up.io/backup: "false".
- Annotate the per-pod cache volumeClaimTemplate the same way.
- Propagate the annotation to the CNPG data PVCs via inheritedMetadata
  (postgres has its own barmanObjectStore backup).
- Leave jellyfin-config unannotated so it remains the only backup target.
jellyfin: back media PVCs with shared static CephFS PVs
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
f5a721cd0b
arrstack: add sonarr/radarr/prowlarr/nzbget on shared CephFS media
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
5c854e61fe
unkin-agent added 1 commit 2026-08-15 13:43:52 +10:00
Merge remote-tracking branch 'origin/main' into benvin/media-arr-apps
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
0cd2f982c2
# Conflicts:
#	argocd/applicationsets/media.yaml
benvin merged commit fc08f1fecc into main 2026-08-15 16:22:07 +10:00
benvin deleted branch benvin/media-arr-apps 2026-08-15 16:22:07 +10:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unkin/argocd-apps#366