Compare commits

..

47 Commits

Author SHA1 Message Date
gilgamezh 5e1a919721 fix(nzbget): render newshosting creds via init container
nzbget does not expand OS env vars in nzbget.conf (its ${...} only
references other nzbget options), so the previous secretKeyRef-as-env
approach left the literal ${NEWSHOSTING_USER} in the config and auth
failed with 400 DENIED.

Add initContainers support to the chart and an init step that seds the
Server1 (newshosting) block into nzbget.conf on every start: non-secret
settings in git, username/password from the usenet-creds Secret. Rotating
the secret + restarting re-renders the creds; no password lands in git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:22:01 +02:00
gilgamezh 6247b140bc feat(nzbget): inject newshosting creds from usenet-creds secret
Reuse the existing nzbget chart as the usenet downloader. Newshosting
username/password come from the out-of-band `usenet-creds` Opaque secret
(same pattern as gluetun-wireguard), exposed as env and referenced in
nzbget.conf via ${NEWSHOSTING_USER}/${NEWSHOSTING_PASS} so no plaintext
provider password lands in the config file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:16:34 +02:00
gilgamezh 98c1e7b63d feat(disk-usage-alert): Telegram alert when turing3 SSD >=90%
The SSD on turing3 (/mnt/ssd) is shared by Postgres, Plex's SQLite DB
and the media library. When it fills, Postgres crashes (cannot write
postmaster.pid) and Plex's library DB corrupts. Add a CronJob that
checks df via the plex-data mount every 15m and warns Telegram at >=90%
used (3h cooldown). Bot token/chatId live in the out-of-band
`telegram-disk-alert` Secret, not in git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:50:01 +02:00
gilgamezh 79a28a674a feat(maintainerr): deploy for watched-movie cleanup (Plex -> Radarr)
Rule-based deletion of watched movies from Radarr (with files), driven by
Maintainerr. Raw manifests + directory-type Argo Application (no Helm).
Config on shared plex-data NFS PVC (subPath configs/maintainerr); Recreate
strategy since it uses SQLite on RWX NFS. ClusterIP only, no ingress —
access via kubectl/k9s port-forward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 21:34:44 +02:00
gilgamezh d6ee993a60 docs: add CloudNativePG migration TODO for postgresql
Plan to move both Bitnami postgres instances (pgsql PG16 in default,
gitea-postgresql PG17 bundled in gitea) to CloudNativePG, since Bitnami
images are frozen (bitnamilegacy). Not executed -- planning doc only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:15:54 +02:00
gilgamezh 2c68a21d0b ops(metallb): upgrade 0.13.12 -> 0.16.1, pin native L2 (no FRR)
0.16.1 chart defaults frr.enabled=false but frrk8s.enabled=true, which
deploys a heavy frr-k8s daemonset. With no BGP peers (pure L2/ARP), FRR is
unnecessary and its images caused DiskPressure on the Pi nodes, evicting a
speaker and stalling the rollout. Disable both frr and frrk8s for a single
-container L2 speaker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:13:59 +02:00
gilgamezh 1b3f34a432 build: upgrade qbittorrent 5.1.4-r3-ls453 -> 5.2.1_v2.0.12-ls459
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 10:59:16 +02:00
gilgamezh 261aebfd10 ops(gitea): Recreate strategy to avoid RWO upgrade deadlock
Bumped gitea helm chart 12.4.0->12.6.0 (app 1.24.6->1.26.1). The chart
default RollingUpdate (maxSurge 100%/maxUnavailable 0) surges a second pod
that can't mount the single RWO NFS PVC, deadlocking 'helm upgrade --wait'.
Recreate avoids it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 10:59:03 +02:00
gilgamezh 9b24978342 docs: add CLAUDE.md (GitOps flow + AirVPN/gluetun DNS gotcha)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 10:20:16 +02:00
gilgamezh 1a91b72464 fix(qbittorrent): use AirVPN plaintext DNS, disable gluetun DoT
AirVPN blocks outbound DNS-over-TLS (tcp/853), so gluetun's default DoT
resolver at 127.0.0.1 never gets answers. The startup healthcheck's
"lookup cloudflare.com" then times out and the VPN restarts every ~6s
in a permanent loop, leaving qbittorrent with no working DNS.

Verified inside the pod netns: tunnel egress works (ping 8.8.8.8 18ms),
AirVPN's pushed resolver 10.128.0.1 resolves fine, but tcp/853 to both
1.1.1.1 and 8.8.8.8 times out.

Set DOT=off and DNS_ADDRESS=10.128.0.1 so gluetun points resolv.conf at
AirVPN's pushed DNS, reached over the tunnel (no DNS leak, no port 853).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 10:17:11 +02:00
argocd-image-updater ac637adaf4 build: automatic update of plex
updates image linuxserver/plex tag '1.43.1.10611-1e34174b1-ls306' to '1.43.2.10687-563d026ea-ls307'
2026-05-19 19:33:41 +00:00
argocd-image-updater 6082e6fc14 build: automatic update of plex
updates image linuxserver/plex tag '1.43.1.10611-1e34174b1-ls305' to '1.43.1.10611-1e34174b1-ls306'
2026-05-18 12:53:50 +00:00
gilgamezh 7e0a38d65f build: pin gluetun to v3.41.1
Fixes VPN restart loop after :latest pulled a build with the Alpine 3.22
iptables parsing regression and the healthcheck race (#3123). v3.41.1
includes the k8s cluster-DNS auto-detection so DNS lookups in the
startup healthcheck no longer time out behind the killswitch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 09:24:50 +02:00
gilgamezh 3b480d6abf build: backup traefik HelmChartConfig from k3s master manifests
Snapshot of /var/lib/rancher/k3s/server/manifests/traefik-config.yaml
on turing1 after dropping the v2 image pin during the Traefik v3
migration. Lives only on the node otherwise — track it here so it can
be restored on a node rebuild.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 09:24:50 +02:00
gilgamezh 3ace05a695 build: migrate ingresses for Traefik v3 (k3s upgrade)
k3s update bumped Traefik chart 37 → 39, dropping v2 support. Replace
the v2-only `whitelist.sourcerange` annotation on the gitea ingress
with an `ipAllowList` Middleware (resources/gitea-middleware.yaml),
referenced via `router.middlewares`. Switch the default-ns ingresses
(kube-plex, radarr, sonarr, lidarr) from the deprecated
`kubernetes.io/ingress.class` annotation to `spec.ingressClassName`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 09:24:50 +02:00
argocd-image-updater 290ce6a103 build: automatic update of plex
updates image linuxserver/plex tag '1.43.1.10611-1e34174b1-ls304' to '1.43.1.10611-1e34174b1-ls305'
2026-05-11 12:31:16 +00:00
gilgamezh e230129119 build: use Recreate strategy for qbittorrent
qbittorrent holds an exclusive lockfile on /config; rolling updates
deadlock because the new pod can't acquire the lock until the old one
is gone.
2026-05-07 10:03:16 +02:00
gilgamezh 269ab53002 build: pin qbittorrent to 5.1.4-r3-ls453 2026-05-07 09:51:40 +02:00
gilgamezh 724568e08f build: pin qbittorrent to 5.1.4 (linuxserver has no libtorrent2 5.2.0) 2026-05-07 09:49:11 +02:00
gilgamezh 770125e7c8 build: update qbittorrent to 5.2.0 and unstick prowlarr
prowlarr was pinned to a stale digest (v2.0.5.5160) via
.argocd-source-prowlarr.yaml; remove the file so the live app's
helm.parameters (which already has the current :latest digest =
v2.3.5.5327) takes effect.

qbittorrent: bump 5.1.0 -> 5.2.0.
2026-05-07 09:46:18 +02:00
argocd-image-updater 3239a1e729 build: automatic update of plex
updates image linuxserver/plex tag '1.43.1.10611-1e34174b1-ls303' to '1.43.1.10611-1e34174b1-ls304'
2026-05-04 11:24:15 +00:00
argocd-image-updater d476af6cfd build: automatic update of plex
updates image linuxserver/plex tag '1.43.1.10611-1e34174b1-ls302' to '1.43.1.10611-1e34174b1-ls303'
2026-04-27 11:20:51 +00:00
argocd-image-updater 930ede9b74 build: automatic update of plex
updates image linuxserver/plex tag '1.43.1.10611-1e34174b1-ls301' to '1.43.1.10611-1e34174b1-ls302'
2026-04-20 11:05:41 +00:00
argocd-image-updater a31e50f02f build: automatic update of plex
updates image linuxserver/plex tag '1.43.1.10576-06378bdcd-ls300' to '1.43.1.10611-1e34174b1-ls301'
2026-04-10 14:05:19 +00:00
argocd-image-updater aa89a5f238 build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10492-121068a07-ls299' to '1.43.1.10576-06378bdcd-ls300'
2026-04-08 15:58:38 +00:00
argocd-image-updater 41e272c9f2 build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10492-121068a07-ls298' to '1.43.0.10492-121068a07-ls299'
2026-04-06 10:26:04 +00:00
argocd-image-updater 4fe6ef579c build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10492-121068a07-ls297' to '1.43.0.10492-121068a07-ls298'
2026-03-30 10:35:06 +00:00
argocd-image-updater 8ea4086a37 build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10492-121068a07-ls296' to '1.43.0.10492-121068a07-ls297'
2026-03-21 14:45:07 +00:00
argocd-image-updater 3827ad656a build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10492-121068a07-ls295' to '1.43.0.10492-121068a07-ls296'
2026-03-15 21:59:52 +00:00
argocd-image-updater 9ab1659939 build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10492-121068a07-ls294' to '1.43.0.10492-121068a07-ls295'
2026-03-02 10:19:10 +00:00
argocd-image-updater 4a66fdfabf build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10492-121068a07-ls293' to '1.43.0.10492-121068a07-ls294'
2026-02-23 10:12:30 +00:00
argocd-image-updater c98f1b93b5 build: automatic update of plex
updates image linuxserver/plex tag '1.42.2.10156-f737b826c-ls292' to '1.43.0.10492-121068a07-ls293'
2026-02-11 16:35:08 +00:00
argocd-image-updater 377fa8ec4a build: automatic update of plex
updates image linuxserver/plex tag '1.42.2.10156-f737b826c-ls291' to '1.42.2.10156-f737b826c-ls292'
2026-02-09 10:20:10 +00:00
argocd-image-updater d009a61c0e build: automatic update of plex
updates image linuxserver/plex tag '1.43.0.10467-2b1ba6e69-ls290' to '1.42.2.10156-f737b826c-ls291'
2026-01-28 21:21:09 +00:00
argocd-image-updater eeb80c2662 build: automatic update of plex
updates image linuxserver/plex tag '1.42.2.10156-f737b826c-ls289' to '1.43.0.10467-2b1ba6e69-ls290'
2026-01-27 19:54:58 +00:00
argocd-image-updater 24a2463f20 build: automatic update of plex
updates image linuxserver/plex tag '1.42.2.10156-f737b826c-ls288' to '1.42.2.10156-f737b826c-ls289'
2026-01-19 09:53:36 +00:00
gilgamezh 5e80bac19d fix torrent path 2026-01-18 14:35:48 +01:00
gilgamezh d39c8ff550 update torrent port 2026-01-18 14:26:47 +01:00
gilgamezh d79e75fa88 update mount path 2026-01-18 14:22:55 +01:00
gilgamezh c773b6da26 update mount path 2026-01-18 14:13:34 +01:00
gilgamezh ba2c36b6f2 media: set cross-seed config dir env 2026-01-18 13:49:28 +01:00
gilgamezh 988a44b609 media: run cross-seed via explicit command 2026-01-18 13:45:00 +01:00
gilgamezh 1b904fe20d media: bind cross-seed daemon to pod IP 2026-01-18 13:38:14 +01:00
gilgamezh 9e4e0d7a9a media: drop unsupported cross-seed config flag 2026-01-18 13:35:29 +01:00
gilgamezh 78ba3041f1 media: run cross-seed with config args 2026-01-18 13:31:41 +01:00
gilgamezh 5988d0df38 media: run cross-seed daemon explicitly 2026-01-18 13:25:27 +01:00
gilgamezh 9a6d7670f4 media: fix cross-seed config for qbittorrent 2026-01-18 13:21:41 +01:00
22 changed files with 614 additions and 34 deletions
+39
View File
@@ -0,0 +1,39 @@
# turingpi
Home k3s cluster on a Turing Pi (arm64 nodes), GitOps-managed by ArgoCD.
## Deploying changes
- Apps are ArgoCD `Application`s in `applications/*.yaml`, pointing at an **internal Gitea**
repo (`gitea-http.gitea.svc.cluster.local`), `targetRevision: HEAD`.
- The git remote is `gitea` (`git@192.168.222.26:admin/turingpi.git`); working branch is **`master`**.
- To deploy: **commit and push to `gitea/master`**. Apps have `syncPolicy.automated` with
`selfHeal: true`, so direct `kubectl patch`/`edit` is reverted — changes must go through git.
- Argo polls Gitea every ~3 min. Force a sync with:
`kubectl -n argocd annotate application <name> argocd.argoproj.io/refresh=hard --overwrite`
- Helm values: `helm-values/<app>_values.yaml`. Custom charts: `custom_helm_charts/<app>/`.
## Gotchas
### qbittorrent + gluetun (AirVPN) — DNS / restart loop
AirVPN blocks outbound **DNS-over-TLS (tcp/853)** to force its own resolver. Gluetun's default
`DOT=on` resolver (127.0.0.1) therefore never gets answers, **all DNS fails**, and the VPN
startup healthcheck (`lookup cloudflare.com`) times out — gluetun restarts the VPN every ~6s in a
permanent loop. The pod still shows `2/2 Running` with 0 restarts, so it looks healthy while
having no usable network.
The gluetun sidecar in `helm-values/qbittorrent_values.yaml` **must** keep:
```yaml
- name: DOT
value: "off"
- name: DNS_ADDRESS
value: "10.128.0.1" # AirVPN's pushed resolver, reached over the tunnel — no DNS leak
```
Diagnose: gluetun logs repeat `restarting VPN ... lookup ... i/o timeout`. Confirm with
`ping 8.8.8.8` (works) and `nslookup x 10.128.0.1` (works) but `curl 1.1.1.1:853` (times out).
Note: the `7e0a38d` "pin gluetun to v3.41.1" commit message falsely claimed v3.41.1 fixed this
DNS timeout. It did not — don't trust that claim.
+174
View File
@@ -0,0 +1,174 @@
# TODO: Migrate PostgreSQL to CloudNativePG
**Status:** planned, not started
**Target:** [CloudNativePG](https://cloudnative-pg.io/) (CNPG operator)
**Created:** 2026-05-31
## Why
Both Postgres instances run **Bitnami** images, which Bitnami froze in
Aug 2025 (free images moved to the `bitnamilegacy` archive and stopped
getting updates / security patches):
| Instance | Namespace | Version | Image | How deployed | Consumers |
|---|---|---|---|---|---|
| `pgsql` | `default` | PG **16.2** | `docker.io/bitnami/postgresql:16.2.0-debian-12-r8` (non-legacy, tag effectively gone from the registry) | standalone helm release `pgsql` (chart `postgresql-15.1.2`) | lidarr, radarr, sonarr, prowlarr, alhfmf |
| `gitea-postgresql` | `gitea` | PG **17.6** | `bitnamilegacy/postgresql:17.6.0-debian-12-r4` | **bundled subchart** inside the `gitea` helm release | gitea only |
CloudNativePG is the chosen replacement: actively maintained, operator-managed
HA/backups, first-class `pg_dump`-based import for migration, and not tied to
Bitnami.
> ⚠️ `pgsql` is on the **non-legacy** `bitnami/` path whose `16.2.0` tag is no
> longer pullable — if that pod is ever rescheduled to a node without the image
> cached, it will **fail to start**. Treat instance A as the higher priority.
## Databases to migrate
- **pgsql (PG16):** `alhfmf`, `lidarr_db`, `lidarr_db_log`, `radarr_db`,
`radarr_db_log`, `sonarr_db`, `sonarr_db_log`, `prowlarr_db`, `prowlarr_db_log`
(confirm full list at cutover with `\l`).
- **gitea (PG17):** `gitea` (owner role `gitea`).
---
## 0. Prerequisites
- [ ] Install the CNPG operator (pin a recent stable version):
```bash
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update cnpg
helm upgrade --install cnpg cnpg/cloudnative-pg \
-n cnpg-system --create-namespace --wait
kubectl get deploy -n cnpg-system # operator Running
```
- [ ] Decide on storage: reuse `nfs-client` StorageClass, **or** prefer local
storage for the DB PVCs (Postgres on NFS is workable but not ideal;
CNPG defaults to RWO). Pick a `storageClass` + size per cluster below.
- [ ] Decide CNPG topology: homelab can run `instances: 1` (no HA) to keep it
light on the Pi nodes; bump to 23 later if desired.
- [ ] Take a manual backup of both instances first (safety net):
```bash
kubectl exec -n default pgsql-postgresql-0 -- \
bash -c 'PGPASSWORD=$POSTGRES_PASSWORD pg_dumpall -U postgres' > pgsql-all.sql
kubectl exec -n gitea gitea-postgresql-0 -- \
bash -c 'PGPASSWORD=$POSTGRES_PASSWORD pg_dumpall -U postgres' > gitea-all.sql
```
---
## Instance A — `pgsql` (media *arr + alhfmf), PG16
CNPG can pull data straight from the old server at bootstrap via
`initdb.import` (it runs `pg_dump`/`pg_restore` for you). Use **monolith** type
to copy all listed databases + roles in one shot.
- [ ] Keep the old `pgsql` release **running** during migration (read source).
- [ ] Create a secret with the old server's superuser creds for the importer:
```bash
# password: kubectl get secret pgsql-postgresql -n default -o jsonpath='{.data.postgres-password}' | base64 -d
kubectl create secret generic pg16-source-superuser -n default \
--from-literal=username=postgres --from-literal=password='<OLD_PW>'
```
- [ ] Apply a CNPG `Cluster` with import bootstrap (sketch — tune names/size):
```yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg16
namespace: default
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:16 # match major 16
storage:
size: 10Gi
storageClass: nfs-client # or local SC — see prereqs
bootstrap:
initdb:
import:
type: monolith
databases: ["alhfmf","lidarr_db","lidarr_db_log","radarr_db","radarr_db_log","sonarr_db","sonarr_db_log","prowlarr_db","prowlarr_db_log"]
roles: ["*"]
source:
externalCluster: old-pgsql
externalClusters:
- name: old-pgsql
connectionParameters:
host: pgsql-postgresql.default.svc.cluster.local
user: postgres
dbname: postgres
password:
name: pg16-source-superuser
key: password
```
- [ ] Wait for import to finish: `kubectl get cluster pg16 -n default -w`
(phase → `Cluster in healthy state`). Check logs of the bootstrap job.
- [ ] Verify row counts / `\dt` in a couple of DBs vs. the old server.
- [ ] **Cutover:** the new service is `pg16-rw.default.svc.cluster.local:5432`.
Update each consumer's DB host:
- [ ] lidarr — `custom_helm_charts/lidarr` / `helm-values/lidarr_values.yaml`
- [ ] radarr — `helm-values/radarr_values.yaml`
- [ ] sonarr — `helm-values/sonarr_values.yaml`
- [ ] prowlarr — `custom_helm_charts/prowlarr` / `helm-values/prowlarr_values.yml`
- [ ] alhfmf — `alhfmf` release (find its DB env/secret)
(the *arr apps store DB host/creds in their `config.xml`/Postgres env —
confirm where each one is configured before flipping.)
Commit + push so Argo redeploys; verify each app reconnects.
- [ ] Soak for a few days.
- [ ] Decommission old: `helm uninstall pgsql -n default`, delete its PVC
(`data-pgsql-postgresql-0`) and `resources/pgsql_persistent_volume.yml`,
remove `non_argo_values/pgsql_values.yaml`, drop `pg16-source-superuser`.
---
## Instance B — `gitea` DB, PG17
Trickier because Postgres is a **subchart of the gitea release**, so it must be
externalized from the gitea chart.
- [ ] Stand up a CNPG `Cluster` `pg17` in the `gitea` ns, PG major **17**
(`imageName: ghcr.io/cloudnative-pg/postgresql:17`), same import approach:
`databases: ["gitea"]`, `roles: ["*"]`, source =
`gitea-postgresql.gitea.svc.cluster.local`, superuser secret from
`kubectl get secret gitea-postgresql -n gitea -o jsonpath='{.data.postgres-password}'`.
- [ ] **Quiesce gitea during final cutover** (scale gitea deploy to 0) so the
source DB is static, then run/refresh the import (or do a final `pg_dump`
of `gitea` and restore into `pg17`) to avoid losing writes.
- [ ] Edit `non_argo_values/gitea_values.yaml`:
- set `postgresql.enabled: false` (drop the bundled subchart)
- point `gitea.config.database` at the CNPG service:
```yaml
gitea:
config:
database:
DB_TYPE: postgres
HOST: pg17-rw.gitea.svc.cluster.local:5432
NAME: gitea
USER: gitea
PASSWD: <from CNPG-managed secret pg17-app>
```
(CNPG creates an app user/secret; either reuse the migrated `gitea` role or
wire gitea to the `pg17-app` secret.)
- [ ] `helm upgrade gitea ...` (Recreate strategy already set). Bring gitea back
up; verify login + that the GitOps repo (`admin/turingpi.git`) is intact —
**Argo depends on this**, so validate before moving on.
- [ ] Decommission: once `postgresql.enabled: false` is live, the old
`gitea-postgresql` StatefulSet is removed by the chart; delete leftover
PVC `data-gitea-postgresql-0` after confirming the new DB is good.
---
## Rollback
- Old instances stay intact until the explicit decommission steps — to roll
back, just point the consumer/gitea config back at the original
`*-postgresql` service. Keep the `pg_dumpall` files until both soaks pass.
## Open questions / decisions
- [ ] NFS vs local storage for DB PVCs (perf + RWO behavior on node loss).
- [ ] HA (`instances: 1` vs `3`) given Pi resource limits.
- [ ] Backups: configure CNPG scheduled backups (Barman to NFS or object store)
as part of this — replaces whatever (if any) backup the Bitnami charts did.
- [ ] Confirm exact per-app DB connection config for the 5 consumers of `pgsql`
before cutover (where each *arr stores its Postgres host/creds).
+23
View File
@@ -0,0 +1,23 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: disk-usage-alert
namespace: argocd
spec:
project: default
source:
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/admin/turingpi.git
targetRevision: HEAD
path: manifests/disk-usage-alert
directory:
recurse: false
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
+23
View File
@@ -0,0 +1,23 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: maintainerr
namespace: argocd
spec:
project: default
source:
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/admin/turingpi.git
targetRevision: HEAD
path: manifests/maintainerr
directory:
recurse: false
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
+37
View File
@@ -0,0 +1,37 @@
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
logs:
access:
enabled: true
format: common
# opcional: para logs de Traefik (no sólo access logs)
log:
level: INFO
format: json
# esto ya estaba, pero si querés mantenerlo:
deployment:
podAnnotations:
prometheus.io/port: "8082"
prometheus.io/scrape: "true"
providers:
kubernetesIngress:
publishedService:
enabled: true
priorityClassName: "system-cluster-critical"
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
service:
ipFamilyPolicy: "PreferDualStack"
@@ -10,12 +10,20 @@ metadata:
data:
config.js: |
module.exports = {
qbittorrentUrl: "{{ .Values.config.qbittorrentUrl }}",
qbittorrentUsername: process.env.QBITTORRENT_USERNAME,
qbittorrentPassword: process.env.QBITTORRENT_PASSWORD,
torrentClients: [
{
type: "qbittorrent",
url: "{{ .Values.config.qbittorrentUrl }}",
username: process.env.QBITTORRENT_USERNAME,
password: process.env.QBITTORRENT_PASSWORD
}
],
host: "{{ .Values.config.host }}",
port: {{ .Values.config.port }},
torrentDir: "{{ .Values.config.torrentDir }}",
outputDir: "{{ .Values.config.outputDir }}",
linkDirs: {{ toJson .Values.config.linkDirs }},
dataDirs: {{ toJson .Values.config.dataDirs }},
outputDir: {{ toJson .Values.config.outputDir }},
action: "{{ .Values.config.action }}",
torznab: [
`{{ .Values.config.torznabBaseUrl }}?t=search&apikey=${process.env.NZBGEEK_API_KEY}`
]
@@ -25,8 +25,14 @@ spec:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.command }}
command:
{{ toYaml . | indent 12 }}
{{- end }}
{{- with .Values.args }}
args:
{{ toYaml .Values.command | indent 12 }}
{{ toYaml . | indent 12 }}
{{- end }}
env:
{{ toYaml .Values.env | indent 12 }}
ports:
+15 -3
View File
@@ -8,6 +8,10 @@ image:
env:
- name: TZ
value: "Europe/Amsterdam"
- name: CONFIG_DIR
value: "/config"
- name: DOCKER_ENV
value: "true"
- name: QBITTORRENT_USERNAME
valueFrom:
secretKeyRef:
@@ -25,14 +29,19 @@ env:
key: NZBGEEK_API_KEY
command:
- "cross-seed"
args:
- "daemon"
config:
qbittorrentUrl: "http://qbittorrent.default.svc.cluster.local:8080"
torrentDir: "/data/torrents"
outputDir: "/data/torrents/.cross-seed"
linkDirs:
host: "0.0.0.0"
port: 2468
torrentDir: "/qbittorrent-config/qBittorrent/BT_backup"
dataDirs:
- "/data/torrents"
outputDir: null
action: "inject"
torznabBaseUrl: "https://api.nzbgeek.info/api"
secret:
@@ -74,6 +83,9 @@ volumeMounts:
- name: config
mountPath: "/config/config.js"
subPath: "config.js"
- name: plex-data
mountPath: "/qbittorrent-config"
subPath: "configs/qbittorrent"
- name: plex-data
mountPath: "/data/torrents"
subPath: "torrent"
@@ -21,6 +21,10 @@ spec:
spec:
volumes:
{{ toYaml .Values.volumes | indent 6 }}
{{- with .Values.initContainers }}
initContainers:
{{ toYaml . | indent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
@@ -1,8 +0,0 @@
helm:
parameters:
- name: image.repository
value: lscr.io/linuxserver/prowlarr
forcestring: true
- name: image.tag
value: latest@sha256:4f2a6d597845b2f3e19284b1d982b3e0b4bd7c22472c2979c956aa198b83f472
forcestring: true
@@ -9,6 +9,8 @@ metadata:
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: Recreate
selector:
matchLabels:
app: {{ template "qbittorrent.name" . }}
+15 -3
View File
@@ -8,6 +8,10 @@ image:
env:
- name: TZ
value: "Europe/Amsterdam"
- name: CONFIG_DIR
value: "/config"
- name: DOCKER_ENV
value: "true"
- name: QBITTORRENT_USERNAME
valueFrom:
secretKeyRef:
@@ -25,14 +29,19 @@ env:
key: NZBGEEK_API_KEY
command:
- "cross-seed"
args:
- "daemon"
config:
qbittorrentUrl: "http://qbittorrent.default.svc.cluster.local:8080"
torrentDir: "/data/torrents"
outputDir: "/data/torrents/.cross-seed"
linkDirs:
host: "0.0.0.0"
port: 2468
torrentDir: "/qbittorrent-config/qBittorrent/BT_backup"
dataDirs:
- "/data/torrents"
outputDir: null
action: "inject"
torznabBaseUrl: "https://api.nzbgeek.info/api"
secret:
@@ -61,6 +70,9 @@ volumeMounts:
- name: config
mountPath: "/config/config.js"
subPath: "config.js"
- name: plex-data
mountPath: "/qbittorrent-config"
subPath: "configs/qbittorrent"
- name: plex-data
mountPath: "/data/torrents"
subPath: "torrent"
+43
View File
@@ -23,6 +23,49 @@ env:
- name: NO_PROXY
value: "localhost,127.0.0.1,.svc,.cluster.local"
# nzbget cannot read server credentials from environment variables (its
# ${...} config syntax only references other nzbget options, not env). So an
# init container renders the Server1 (newshosting) block into nzbget.conf on
# every start: the non-secret settings live here in git, while the username
# and password come from the out-of-band `usenet-creds` Secret (same pattern
# as gluetun-wireguard — secret not committed). Rotating the secret + a pod
# restart re-renders the creds. No provider password is ever stored in git.
initContainers:
- name: render-newshosting
image: lscr.io/linuxserver/nzbget:latest
command:
- sh
- -c
- |
f=/config/nzbget.conf
[ -f "$f" ] || { echo "nzbget.conf absent; main container will seed defaults"; exit 0; }
sed -i \
-e "s|^Server1.Active=.*|Server1.Active=yes|" \
-e "s|^Server1.Name=.*|Server1.Name=newshosting|" \
-e "s|^Server1.Host=.*|Server1.Host=news.newshosting.com|" \
-e "s|^Server1.Port=.*|Server1.Port=563|" \
-e "s|^Server1.Encryption=.*|Server1.Encryption=yes|" \
-e "s|^Server1.Connections=.*|Server1.Connections=30|" \
-e "s|^Server1.Username=.*|Server1.Username=${NEWSHOSTING_USER}|" \
-e "s|^Server1.Password=.*|Server1.Password=${NEWSHOSTING_PASS}|" \
"$f"
echo "rendered newshosting Server1 block into nzbget.conf"
env:
- name: NEWSHOSTING_USER
valueFrom:
secretKeyRef:
name: usenet-creds
key: NEWSHOSTING_USER
- name: NEWSHOSTING_PASS
valueFrom:
secretKeyRef:
name: usenet-creds
key: NEWSHOSTING_PASS
volumeMounts:
- name: plex-data
mountPath: /config
subPath: configs/nzbget
service:
type: ClusterIP
port: 6789
+1 -1
View File
@@ -1,7 +1,7 @@
claimToken: "claim-E_NxQDtUMMVsLCBFvybK"
image:
repository: linuxserver/plex
tag: "1.42.2.10156-f737b826c-ls288"
tag: "1.43.2.10687-563d026ea-ls307"
pullPolicy: Always
kubePlex:
enabled: false # kubePlex (transcoder job) is disabled because not available on ARM. The transcoding will be performed by the main Plex instance instead of a separate Job.
+20 -7
View File
@@ -1,9 +1,10 @@
---
replicaCount: 1
qbittorrent:
image:
repository: lscr.io/linuxserver/qbittorrent
tag: "5.1.0"
tag: "5.2.1_v2.0.12-ls459"
pullPolicy: Always
env:
- name: PUID
@@ -15,8 +16,8 @@ qbittorrent:
- name: WEBUI_PORT
value: "8080"
- name: TORRENTING_PORT
value: "6881"
torrentPort: 6881
value: "54408"
torrentPort: 54408
livenessProbe:
tcpSocket:
port: 8080
@@ -45,13 +46,16 @@ qbittorrent:
mountPath: "/config"
subPath: "configs/qbittorrent"
- name: plex-data
mountPath: "/data/torrents"
mountPath: "/nfs/incomplete_torrents"
subPath: "incomplete_torrents"
- name: plex-data
mountPath: "/nfs/torrent"
subPath: "torrent"
gluetun:
image:
repository: qmcgaw/gluetun
tag: latest
tag: v3.41.1
pullPolicy: IfNotPresent
env:
- name: VPN_SERVICE_PROVIDER
@@ -72,10 +76,18 @@ gluetun:
value: "10.160.17.207/32,fd7d:76ee:e68f:a993:61d7:a5fe:f834:90e1/128"
- name: SERVER_COUNTRIES
value: "Netherlands"
# AirVPN blocks outbound DNS-over-TLS (tcp/853), so gluetun's default
# DoT resolver never gets answers and the startup healthcheck loops
# forever on "lookup cloudflare.com: i/o timeout". Use AirVPN's pushed
# plaintext resolver instead (reached over the tunnel, no DNS leak).
- name: DOT
value: "off"
- name: DNS_ADDRESS
value: "10.128.0.1"
- name: FIREWALL_INPUT_PORTS
value: "8080"
- name: FIREWALL_VPN_INPUT_PORTS
value: "6881"
value: "54408"
- name: TZ
value: "Europe/Amsterdam"
securityContext:
@@ -120,7 +132,8 @@ volumes:
hostPath:
path: /dev/net/tun
nodeSelector: {}
nodeSelector:
kubernetes.io/arch: arm64
tolerations: []
+81
View File
@@ -0,0 +1,81 @@
# SSD disk-usage alert for turing3 /mnt/ssd (shared by Postgres + Plex + media).
# A full SSD crashes Postgres and corrupts Plex's SQLite DB, so we warn early.
#
# NOTE: the Telegram bot token + chat id live in the `telegram-disk-alert` Secret,
# created out-of-band (NOT in git) so the token stays out of history:
# kubectl -n default create secret generic telegram-disk-alert \
# --from-literal=botToken='<token>' --from-literal=chatId='<chatId>'
#
# Mounts the existing RWX plex-data PVC purely to read `df` of the underlying SSD.
apiVersion: batch/v1
kind: CronJob
metadata:
name: disk-usage-alert
namespace: default
spec:
schedule: "*/15 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
restartPolicy: Never
containers:
- name: check
image: curlimages/curl:8.11.1
imagePullPolicy: IfNotPresent
env:
- name: THRESHOLD # alert at/above this % used
value: "90"
- name: COOLDOWN_SEC # min seconds between alerts (3h)
value: "10800"
- name: MOUNT
value: "/data"
- name: BOT_TOKEN
valueFrom:
secretKeyRef: { name: telegram-disk-alert, key: botToken }
- name: CHAT_ID
valueFrom:
secretKeyRef: { name: telegram-disk-alert, key: chatId }
command: ["/bin/sh", "-c"]
args:
- |
set -u
pct=$(df -P "$MOUNT" | awk 'END{gsub("%","",$5); print $5}')
avail=$(df -Ph "$MOUNT" | awk 'END{print $4}')
now=$(date +%s)
marker="$MOUNT/.disk-alert-last"
send() {
curl -s -m 15 "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${CHAT_ID}" \
--data-urlencode "text=$1" -d "parse_mode=HTML" >/dev/null
}
if [ "${TEST:-0}" = "1" ]; then
send "✅ <b>turingpi disk-alert test</b> — SSD at ${pct}% (free ${avail}). Alerting works."
echo "TEST sent (${pct}% used)"; exit 0
fi
if [ "$pct" -ge "$THRESHOLD" ]; then
last=0; [ -f "$marker" ] && last=$(cat "$marker" 2>/dev/null || echo 0)
if [ $((now - last)) -ge "$COOLDOWN_SEC" ]; then
send "⚠️ <b>turingpi SSD ${pct}% full</b> — only ${avail} free on /mnt/ssd. Postgres + Plex crash at 100%. Prune content / check maintainerr."
echo "$now" > "$marker"
echo "ALERT sent (${pct}% >= ${THRESHOLD}%)"
else
echo "over threshold (${pct}%) but within cooldown; skipping"
fi
else
echo "ok: ${pct}% used, ${avail} free (threshold ${THRESHOLD}%)"
fi
resources:
requests: { cpu: 10m, memory: 16Mi }
limits: { memory: 64Mi }
volumeMounts:
- { name: data, mountPath: /data }
volumes:
- name: data
persistentVolumeClaim:
claimName: plex-data
+63
View File
@@ -0,0 +1,63 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: maintainerr
namespace: default
labels:
app: maintainerr
spec:
replicas: 1
# SQLite on the shared NFS PVC: never run two writers at once.
strategy:
type: Recreate
selector:
matchLabels:
app: maintainerr
template:
metadata:
labels:
app: maintainerr
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: maintainerr
image: ghcr.io/maintainerr/maintainerr:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 6246
protocol: TCP
env:
- name: TZ
value: "Europe/Amsterdam"
volumeMounts:
- name: plex-data
mountPath: /opt/data
subPath: configs/maintainerr
readinessProbe:
httpGet:
path: /api/app/status
port: http
initialDelaySeconds: 20
periodSeconds: 15
livenessProbe:
httpGet:
path: /api/app/status
port: http
initialDelaySeconds: 60
periodSeconds: 30
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "1000m"
volumes:
- name: plex-data
persistentVolumeClaim:
claimName: plex-data
+17
View File
@@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Service
metadata:
name: maintainerr
namespace: default
labels:
app: maintainerr
spec:
type: ClusterIP
ports:
- port: 6246
targetPort: http
protocol: TCP
name: http
selector:
app: maintainerr
+8 -2
View File
@@ -4,6 +4,12 @@
# Single replica for homelab
replicaCount: 1
# Gitea data lives on a single RWO NFS PVC, so two pods can't run at once.
# The chart default (RollingUpdate maxSurge 100%/maxUnavailable 0) surges a
# second pod and deadlocks on upgrade -- use Recreate instead.
strategy:
type: Recreate
# Service configuration - LoadBalancer for direct access
service:
http:
@@ -21,8 +27,8 @@ ingress:
className: traefik
pathType: Prefix
annotations:
# Restrict to LAN access (matching your existing pattern)
traefik.ingress.kubernetes.io/whitelist.sourcerange: "192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"
# Restrict to LAN access via Traefik v3 Middleware (resources/gitea-middleware.yaml)
traefik.ingress.kubernetes.io/router.middlewares: "gitea-lan-only@kubernetescrd"
cert-manager.io/cluster-issuer: "letsencrypt-production"
hosts:
- host: gitea.gilgamezh.me
+13
View File
@@ -0,0 +1,13 @@
# MetalLB configuration for TuringPi K3s cluster
#
# L2 (ARP) mode only: the cluster advertises LoadBalancer IPs via the default
# IPAddressPool + L2Advertisement (resources/metallb.yml). There are no BGP
# peers, so FRR is not needed. Disable both the legacy embedded FRR and the
# newer frr-k8s daemonset -- their images cause DiskPressure on the small Pi
# nodes and add no value without BGP.
speaker:
frr:
enabled: false
frrk8s:
enabled: false
+12
View File
@@ -0,0 +1,12 @@
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: lan-only
namespace: gitea
spec:
ipAllowList:
sourceRange:
- 192.168.0.0/16
- 10.0.0.0/8
- 172.16.0.0/12
+4 -4
View File
@@ -4,12 +4,12 @@ kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
kubernetes.io/ingress.class: traefik
labels:
app: kube-plex
name: kube-plex
namespace: default
spec:
ingressClassName: traefik
rules:
- host: tp2.gilgamezh.me
http:
@@ -31,12 +31,12 @@ kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
kubernetes.io/ingress.class: traefik
labels:
app: radarr
name: radarr
namespace: default
spec:
ingressClassName: traefik
rules:
- host: radarr.gilgamezh.me
http:
@@ -58,12 +58,12 @@ kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
kubernetes.io/ingress.class: traefik
labels:
app: sonarr
name: sonarr
namespace: default
spec:
ingressClassName: traefik
rules:
- host: sonarr.gilgamezh.me
http:
@@ -85,12 +85,12 @@ kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
kubernetes.io/ingress.class: traefik
labels:
app: lidarr
name: lidarr
namespace: default
spec:
ingressClassName: traefik
rules:
- host: lidarr.gilgamezh.me
http: