Reclaimerr¶
Jellyfin/Plex disk-space reclamation tool in the media namespace. Identifies and deletes
unwatched or low-rated media, exposing an internal-only web UI via Envoy Gateway. Standard
bjw-s app-template deployment mirroring sibling apps (maintainerr, pulsarr).
Purpose¶
- Reclaim disk by flagging and removing unwatched / low-rated media, integrating with Jellyfin/Plex and the *arr stack (Sonarr/Radarr).
- Single Deployment pod with a small VolSync-backed PVC (
2Gi) at/app/dataholding the app database, logs, and static files. - Jellyfin/Plex and *arr connections are configured through the UI post-deploy, not in Git.
Design decisions¶
- bjw-s
app-templatechart (OCI source, tag matching maintainerr/pulsarr): the standardks.yaml+app/(kustomization, ocirepository, helmrelease) layout. No deviation from the namespace pattern. - No ExternalSecret. Reclaimerr auto-generates
JWT_SECRETandENCRYPTION_KEYon first launch and persists them into/app/data(the upstream-recommended path). This is simpler than 1Password management; the cost is that the keys live only in the PVC, so the PVC is the source of truth for sessions and encrypted state. - Internal-only ingress. Inline
route:on theenvoy-internallistener (namespacenetwork) for the single${SECRET_DOMAIN}hostname: theenvoy-internalattachment is what keeps it internal-only. No Cloudflare/external exposure, consistent with maintainerr. COOKIE_SECURE: "true"because Envoy terminates TLS in front of it, andCORS_ORIGINSis the single originhttps://reclaimerr.${SECRET_DOMAIN}so the SPA's API calls are accepted.- Components:
homepage(dashboard tile under theMediagroup,mdi-broomicon) andvolsync(PVC backup). (Gatus monitoring is automatic via the gatus-sidecar chart's HTTPRoute auto-discovery, nogatus/guardedcomponent.)VOLSYNC_CAPACITY: 2Gioverrides the 5Gi default; the cache size is deliberately not pinned, so it follows the component default. - Deferred (out of scope): external ingress, pre-seeding
JWT_SECRET/ENCRYPTION_KEYvia 1Password, and aTMDB_API_KEYoverride (the bundled upstream key is fine).
Deploy gotchas¶
readOnlyRootFilesystem: trueneeds a writable/tmp. The container security context locks the root filesystem read-only, so atmpemptyDiris mounted at/tmp. Without it the app fails to write scratch files at runtime./app/datais writable via the VolSync PVC.- Health endpoint is a backend JSON route, not an SPA. All three probes (liveness,
readiness, startup) hit
GET /api/info/healthon port8000, and the Gatus condition is[STATUS] == 200(not a redirecting-SPA check). The Gatus endpoint targets the in-cluster service URL on*.svc.cluster.local.
- Startup probe must allow a slow first boot. First launch generates the JWT/encryption
keys and initialises the DB into the empty PVC; size the startup probe generously
(
failureThreshold: 30,periodSeconds: 5) so it does not CrashLoop before init finishes. - Release tag, pinned by digest. The image moved off the beta tag to a
0.5.x@sha256:...release tag in #6104, and Renovate tracks releases. Keep the digest pin so a retagged release cannot silently change the running build. - Component path depth. From
kubernetes/apps/media/reclaimerr/ks.yaml, the component references need four../segments to reachcomponents/. A wrong depth or a missingVOLSYNC_CAPACITYsubstitute fails the flate/flux render.
Notifications¶
- Pushover via Apprise. Reclaimerr has one Apprise notification service, named "Pushover". Like the other connections, it lives in the app's database on the PVC, not in Git.
- Credentials. The Pushover application "Reclaimerr" has its token in the
RECLAIMERR_PUSHOVER_TOKENfield of thepushoveritem in theTalos1Password vault, next to the sharedPUSHOVER_USER_KEY. The Apprise URL ispover://<user key>@<app token>; both halves are credentials, so never print it. - Enabled events: New Cleanup Candidates (top 5 summary), Task Failures, Deletion Execution Failures, Deletion Completed, Deletion Failed, and Your Requests Leaving Soon. Request approved/declined and the admin request events are off (single-user instance). Update Available is off because Renovate already raises release PRs.
- How it was created (2026-10-14). A short Python script run inside the pod
(
PYTHONPATH=/app) sent a test with the app's ownbackend.services.notifications.test_notification_url, then inserted aNotificationSettingrow throughbackend.database.async_db. The URL was piped in on stdin from 1Password, so it never went through the browser. - The settings UI can blank the URL. After a reload the UI showed the Apprise URL box empty, although the settings API returns the stored URL. Saving that entry from the UI may overwrite the URL with a blank one. Change it through the in-pod path instead, or confirm the box is filled before saving.
Operational notes¶
- The PVC is the only state. Losing
/app/dataloses the auto-generatedJWT_SECRETandENCRYPTION_KEY(all sessions invalidated, encrypted config unreadable). Recovery is via the VolSync snapshot/backup of thereclaimerrPVC. There is no 1Password copy of the keys. - Runs unprivileged. Pod security context is
runAsNonRootas UID/GID1000withfsGroup: 1000andfsGroupChangePolicy: OnRootMismatch; the container drops all capabilities and disables privilege escalation. Thecpurequest is10m; thememorylimit is6Gi(#6105, #6111), because a full series sync holds Plex's whole play history in memory and was OOMKilled at2Gi. - First-run setup is manual. After the pod is
Ready, browse to the internal hostname and wire up Jellyfin/Plex and *arr connections through the UI; nothing is pre-seeded. - Verify after merge: PVC
Bound, podRunning/Ready=1/1, logs show it listening on:8000with keys generated into/app/data, the HTTPRoute is accepted byenvoy-internal, andGET /api/info/healthreturns a200JSON body via the internal hostname. - Rollback:
flux suspend helmrelease reclaimerr -n mediathen delete the pod; reverting the commits prunes the Kustomization (prune: true) while VolSync snapshots the PVC per the volsync component defaults.