Scaling¶
The cluster runs one deliberately narrow autoscaling pattern, zeroscaler: an app scales to 0 replicas when its NFS backing store is unreachable and back to 1 when it returns. It is a dependency-availability gate, not load-based autoscaling. There are no CPU/memory HPAs, and no KEDA (removed 2026-07 in favour of a native HPA).
The point is to stop NFS-dependent apps from CrashLoopBackOff-ing against a dead mount (e.g. while the NAS reboots), and to let them self-heal when it comes back.
How it works¶
blackbox-exporter lan-tcp-nfs Probe (jobName: nfs_probe) → probe_success{job="nfs_probe"}
│ (Prometheus)
▼
prometheus-adapter (observability) → serves external.metrics.k8s.io
rule: max_over_time(probe_success[3m]) ← debounce
│
▼
HorizontalPodAutoscaler per app (components/zeroscaler)
minReplicas 0, maxReplicas 1, external metric target 1
│
▼
Deployment/<app> → metric 1 = 1 replica, metric 0 = 0 replicas
- The blackbox
tcp_connectprobe hits the NFS server on:2049every 1m.jobName: nfs_probegives the series a stablejob="nfs_probe"label for the HPA selector. prometheus-adaptertranslates that Prometheus series into a Kubernetes external metric. Themax_over_time(...[3m])wrapper is a debounce: a single transientprobe_success=0(e.g. a blackbox pod reschedule during a Talos drain) no longer trips scale-to-0; the probe must fail continuously for ~3 minutes. See KB-004.- The HPA (
autoscaling/v2, min 0 / max 1) tracks the metric:1holds one replica,0scales to zero.
Gated apps¶
Nine NFS-dependent apps are gated: media: plex, jellyfin, sonarr, radarr, bazarr, pinchflat, wizarr; downloads: qbittorrent, sabnzbd.
Adding an app to the gate¶
Add the component to the app's ks.yaml spec.components (the ${APP} substitution is already
set on every app Kustomization):
Optional per-app overrides via postBuild.substitute:
ZEROSCALER_JOB_NAME: gate on a different blackbox probe (e.g. a Zigbee coordinator instead of NFS).ZEROSCALER_CONTROLLER: StatefulSet: if the app is a StatefulSet rather than a Deployment.
Prerequisites¶
The HPAScaleToZero feature gate must be enabled on both cluster.apiServer.extraArgs and
cluster.controllerManager.extraArgs in talos/patches/controller/cluster.yaml. The apiserver
validates minReplicas (and rejects 0 without the gate); the controller-manager does the actual
scale-to-0. Missing it on the apiserver is the classic failure: see
KB-024.
Operations¶
- Pause during a node drain: native HPAs have no
pausedannotation, so pin every gated app up with the recipe (it patchesminReplicas: 1):
just kube zeroscaler suspend # pin all NFS-gated apps up
just kube zeroscaler resume # back to metric-driven
Flux reverts minReplicas to 0 on the next reconcile, so pause then act promptly (or
flux suspend the app's Kustomization for a longer hold).
- KubeHpaMaxedOut is silenced cluster-wide: every HPA here is maxReplicas: 1, permanently
"maxed" when NFS is up (silence zeroscaler-hpa-maxed).
- helm-controller drift detection stays off: a native HPA writes spec.replicas, and drift
detection would fight it.
Why this shape¶
- No load-based autoscaling. Home-cluster workloads are singletons; scaling on CPU/memory isn't needed. The only useful signal is "is the backing store there?".
- No KEDA. It was a whole operator (+ CRDs + metrics server + webhooks) for this one binary
pattern. A native HPA plus
prometheus-adapteris lighter and keeps the singletonexternal.metrics.k8s.ioAPIService free. KEDA's metrics server and the adapter cannot co-own it.
Deep dive and gotchas: KB-024. Related: Storage, KB-004 (flap during drains).