{{- $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 .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..existingClaim is mounted, never created, and is meant to be shared with your Jellyfin. One claim can serve all three categories via media..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 }}. If the claims already hold data owned by someone else, fix the ownership before the first start — the pod will not do it for you (fsGroupChangePolicy is OnRootMismatch, deliberately, so a large library is not recursively re-chowned on every start). {{- 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 }}