Files
helm-charts/freesailarr/templates/NOTES.txt
T
jklein 39e9613ee3 freesailarr: one volume entry per media claim (2.0.2+up1.0.0)
media.downloads, media.movies and media.tv rendered three fixed volume
entries, one per category. Two categories on the same existingClaim
therefore produced two pod volumes with an identical claimName, and such a
pod never starts: it stays in Init:0/1 without a single kubelet event
(measured on RKE2/Longhorn, RWO and RWX alike).

The volume list is now deduplicated by claim name. A new mediaVolumeName
helper names the entry after the first ENABLED category in the fixed order
downloads, movies, tv that carries the claim; the other categories mount
that same entry and differ only in their subPath. The restriction to
enabled categories matters because the three entries render under
different conditions - a canonical owner whose entry is not rendered would
be a dangling reference.

With three distinct claims the chart renders byte-identically to 2.0.1,
hence a patch bump.

Also corrected in the values documentation, after measuring instead of
assuming: the kubelet creates missing subPath directories itself, but they
inherit the claim root's mode, and fsGroup cannot be relied on to fix that
- Longhorn's fsGroupPolicy (ReadWriteOnceWithFSType) skips fsGroup on RWX
claims entirely. What has to hold is that the claim root is writable for
PUID/PGID. No chown or init container was added.
2026-08-24 21:01:53 +02:00

109 lines
5.4 KiB
Plaintext

{{- $name := printf "%s-%s" .Release.Name .Chart.Name -}}
{{- $replicas := include "freesailarr.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) -}}
{{- /* The effective (null-safe) startup probe, so the budget below matches the
container even when the block has been erased (#99). */ -}}
{{- $startup := fromYaml (include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "startupProbe" "given" .Values.gluetun.startupProbe)) -}}
{{- $budget := mul (int $startup.periodSeconds) (int $startup.failureThreshold) -}}
{{- $ingressApps := include "freesailarr.ingressApps" . | fromJsonArray -}}
{{- $portApps := include "freesailarr.portApps" . | fromJsonArray -}}
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
Nine containers in ONE pod, sharing gluetun's network namespace.
NOTHING IS READY UNTIL THE TUNNEL IS UP
gluetun's readiness probe is its health server, which answers 200 only while
the Wireguard tunnel is actually working. So "pod not Ready" almost always
means "no tunnel", not "app broken". The startup budget is {{ $budget }}s before
Kubernetes starts killing the container:
kubectl -n {{ .Release.Namespace }} logs {{ $name }}-sfs-0 -c gluetun -f
While gluetun is down its firewall keeps blocking every app container's
egress. That is the kill switch doing its job — the apps stay up and simply
reach nothing.
THE 1PASSWORD ITEM AND THE TWO FIELDS IT MUST CONTAIN
secrets.gluetunSecretRef -> {{ .Values.secrets.gluetunSecretRef | default "(not set)" }}
wireguard-private-key
wireguard-addresses
A missing field does not fail the install: gluetun starts with the variable
empty and never completes a handshake.
kubectl -n {{ .Release.Namespace }} get onepassworditem {{ $name }}-gluetun-secret
{{- if not .Values.gluetun.vpn.firewallOutboundSubnets }}
SET gluetun.vpn.firewallOutboundSubnets — IT IS EMPTY RIGHT NOW
Without it the gluetun firewall blocks cluster-internal egress as well:
lookups against cluster DNS and seerr reaching an in-cluster Jellyfin both
fail, in a way that looks like an application bug. Set it to the cluster pod
and service CIDRs, e.g. "10.42.0.0/16,10.43.0.0/16" on k3s.
{{- end }}
ACCESS
{{- if and .Values.ingress.enabled $ingressApps }}
{{- range $ingressApps }}
{{- $cfg := (index $.Values .name).ingress }}
{{ .name }}: https://{{ $cfg.host }}
{{- end }}
Each app has its OWN Ingress and its own TLS secret; while cert-manager is
still issuing, the browser warns:
kubectl -n {{ .Release.Namespace }} get certificate
{{- else }}
No Ingress is rendered{{ if not .Values.ingress.enabled }} (ingress.enabled is false){{ end }}. Reach an app through its own service:
{{- range $portApps }}
kubectl -n {{ $.Release.Namespace }} port-forward svc/{{ $name }}-{{ .name }}-service {{ .port }}:{{ .port }}
{{- end }}
{{- end }}
{{- if .Values.qbittorrent.enabled }}
qBittorrent's WebUI rejects a Host header whose port differs from its own
({{ "8080" }}) with a plain 401. A port-forward on a different local port therefore
looks like a login failure — forward 8080:8080, or go through the ingress.
{{- end }}
EVERY SERVICE POINTS AT THE SAME POD IP
One ClusterIP service per app{{ if .Values.recyclarr.enabled }} (recyclarr has none — no port in daemon mode){{ end }}, but all
containers share gluetun's network namespace, so the services differ only in
their port. They give you stable per-app DNS names, NOT network separation
between the apps.
TOGGLING AN APP OFF AND ON LATER NEEDS ONE MANUAL STEP
The config volumes come from volumeClaimTemplates, and those are immutable.
Changing any <app>.enabled after the first install makes the upgrade fail
until the StatefulSet is recreated — the data survives:
kubectl -n {{ .Release.Namespace }} delete statefulset {{ $name }}-sfs --cascade=orphan
THE MEDIA CLAIMS ARE YOURS, THE CONFIG VOLUMES ARE THE CHART'S
media.<category>.existingClaim is mounted, never created, and is meant to be
shared with your Jellyfin. One claim can serve all three categories via
media.<category>.subPath.
Measured, so you do not plan around something that does not happen: Radarr
and Sonarr do NOT hardlink between the categories, not even on one claim.
Kubernetes makes every volumeMount its own bind mount and link() across two
mount points fails with EXDEV regardless of subPath, so an import copies.
The containers write as {{ .Values.puid }}:{{ .Values.pgid }}. The claim's root directory has to be
writable for them, and a subPath directory the kubelet creates inherits that
root's mode — so fix the ownership before the first start. The pod will not
do it for you: fsGroupChangePolicy is OnRootMismatch so a large library is
not re-chowned on every start, and on an RWX claim a driver like Longhorn
(fsGroupPolicy ReadWriteOnceWithFSType) ignores fsGroup altogether.
{{- if eq $replicas "0" }}
SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}replicas: 0{{ end }} — the StatefulSet runs with replicas: 0. PVCs, secrets,
services{{ if .Values.ingress.enabled }} and ingresses{{ end }} stay in place; set it back and upgrade to bring the
stack up again.
{{- else if ne $replicas "1" }}
MORE THAN ONE REPLICA IS NOT MEANINGFUL
replicas is {{ $replicas }}. Each replica would run its own isolated stack on its own
config volumes, all of them fighting over the same media claims and the same
VPN credentials.
{{- end }}