Files
helm-charts/freesailarr/values.yaml
T
jklein cdb66a32f4 Per-app ingress and service, media subPaths
Three review changes to the freesailarr chart.

Ingress: the chart no longer derives hosts as <app>.<ingress.domain> from one
Ingress object. The global ingress.domain is gone; every app carries its own
<app>.ingress.host and renders its own Ingress <release>-<chart>-<app>-ingress
with its own TLS secret tls-<release>-<chart>-<app>-ingress. The hosts are
therefore independent and may live in unrelated domains. The host is required,
but the required() call sits inside the loop over the *enabled* app ingresses,
so switching an app's ingress off must not make the chart demand a host for it.
clusterIssuer, className and annotations stay global defaults and are
overridable per app; an explicitly set per-app value wins (nil check instead of
"default", so an empty override is honoured).

Media: media.<category>.subPath mounts a sub-directory of the claim instead of
its root, default null (claim root = previous behaviour). The same
existingClaim may back all three categories with different subPaths. That is
the point rather than cosmetics: Radarr and Sonarr import by moving, and a move
within one filesystem can be a hardlink instead of a copy - across volume
boundaries every import duplicates the file.

Services: the collective Service with named ports is replaced by one ClusterIP
Service <release>-<chart>-<app>-service per enabled app that listens on a port,
flaresolverr included; recyclarr serves no port in daemon mode and gets none.
Each ingress points at its own app's service. All containers share gluetun's
network namespace, so the services differ only in their port - they provide
stable per-app DNS names, not network separation. The StatefulSet's governing
serviceName no longer names a removed object.
2026-08-24 13:45:38 +02:00

680 lines
20 KiB
YAML

# freesailarr runs gluetun plus the whole media automation stack in a single
# StatefulSet pod: every app container shares gluetun's network namespace, the
# containers talk to each other over localhost and all traffic leaves through
# the Wireguard tunnel.
#
# Image pinning (documented deviation from the Chart.AppVersion standard):
# appVersion pins the gluetun image only. All other app images are pinned via
# values (<app>.image.repository / <app>.image.tag) because the stack combines
# nine independently versioned upstream projects; the pinned tags are reviewed
# alongside every app-version update round, like side images in other charts.
# Scale the whole chart down without uninstalling it: scaleDown: true renders
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
# takes precedence over any per-workload replicas value. PVCs, Secrets and
# Services stay in place; scaleDown: false brings the workloads back up.
scaleDown: false
# Replica count for the whole stack (one pod contains all containers); 0 is
# allowed and scales it down. The chart-global scaleDown flag takes precedence.
# More than 1 replica is not meaningful: every replica would run its own
# isolated stack on its own config volumes.
replicas: 1
secrets:
# op:// item path for the gluetun VPN credentials. The 1Password item must
# contain the fields "wireguard-private-key" (the Wireguard private key) and
# "wireguard-addresses" (the Wireguard interface address, e.g. 10.x.y.z/32).
gluetunSecretRef: null
# User/group the linuxserver.io containers drop to after init (their PUID/PGID
# mechanism) and the runAsUser/runAsGroup for the seerr and recyclarr
# containers. Media and config volumes must be writable for this user.
puid: 1000
pgid: 1000
# Timezone for all containers.
timezone: Europe/Zurich
securityContext:
# Pod-level security context. No pod-level runAsNonRoot/runAsUser: the pod
# mixes root containers (gluetun, linuxserver.io images) with non-root ones
# (seerr, recyclarr, flaresolverr); users are set per container instead.
# fsGroup (keep in sync with pgid) makes freshly provisioned config volumes
# group-writable for the non-root containers; fsGroupChangePolicy
# OnRootMismatch avoids recursively re-chowning the (large, shared) media
# claims on every pod start.
pod:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
storage:
# StorageClass for all config volumeClaimTemplates (null = cluster default).
storageClass: null
# Media libraries as pre-existing PVCs (shared with the separately deployed
# Jellyfin) - the chart mounts but never creates them. Each claim is required
# as soon as an app that mounts it is enabled:
# downloads: qbittorrent, radarr, sonarr
# movies: radarr, bazarr
# tv: sonarr, bazarr
#
# subPath mounts a sub-directory of the claim instead of its root (null = the
# claim root, i.e. one claim per category). The same existingClaim may be used
# for all three categories with different subPaths - that is more than a
# convenience: Radarr and Sonarr import finished downloads into the library by
# *moving* them, and a move within one filesystem can be a hardlink or rename
# instead of a copy. On one volume that costs no second copy of the data and no
# copy time; across volume boundaries a hardlink is impossible, so every import
# duplicates the file and the download and the library each hold a full copy
# until the torrent is removed. Example: one claim, subPaths downloads /
# movies / tv-shows.
media:
downloads:
existingClaim: null
subPath: null
movies:
existingClaim: null
subPath: null
tv:
existingClaim: null
subPath: null
# Ingress defaults. There is deliberately no global domain: every app carries
# its own <app>.ingress.host, so the hosts are independent of each other and
# may live in completely different domains. clusterIssuer, className and
# annotations are the defaults for all apps and can be overridden per app in
# the <app>.ingress block (an explicitly set per-app value always wins).
ingress:
# Master switch: false suppresses every app ingress regardless of the
# per-app toggles.
enabled: true
clusterIssuer: null
className: nginx
# Extra annotations for the Ingress objects; merged with the cert-manager
# cluster-issuer annotation the chart always sets.
annotations: {}
gluetun:
# Image tag comes from Chart.AppVersion (gluetun is the chart's main app).
vpn:
serviceProvider: mullvad
type: wireguard
serverCities: Zurich
serverCountries: Switzerland
dnsUpstreamResolvers: quad9
# Comma-separated CIDRs the gluetun firewall allows outbound traffic to
# outside the VPN tunnel. Set this to the cluster pod and service CIDRs
# (e.g. "10.42.0.0/16,10.43.0.0/16" on k3s) - without it the firewall
# blocks cluster-internal egress such as DNS lookups against cluster DNS
# and seerr talking to the in-cluster Jellyfin.
firewallOutboundSubnets: null
storage:
# Size of the gluetun config volume (server list cache under /gluetun).
size: 1Gi
# Extra environment variables, appended to the ones the chart always sets.
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
# forms are supported. (Same for every <app>.env block below.)
env: []
# gluetun's built-in HTTP health server; the chart binds it to :9999 (env
# HEALTH_SERVER_ADDRESS) so the kubelet probes, which connect to the pod IP,
# can reach it. It returns 200 only while the VPN tunnel is up and working.
# Liveness is more tolerant (60s) than readiness (30s): during a short
# tunnel re-negotiation gluetun recovers on its own and a kubelet restart
# would only prolong the outage; the kill switch keeps all egress blocked
# the whole time, so nothing runs unprotected.
livenessProbe:
httpGet:
path: /
port: 9999
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
readinessProbe:
httpGet:
path: /
port: 9999
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Budget for image start plus
# Wireguard handshake: 5s * 30 = 150s.
startupProbe:
httpGet:
path: /
port: 9999
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
# Limits are optional: memory and ephemeral-storage limits default to
# limitFactor x the request, an explicitly set limit always wins, and a
# CPU limit is only set when configured explicitly under "limits".
# (Same for every <app>.resources block below.)
limitFactor: 3
requests:
cpu: 0.1
memory: 128Mi
ephemeralStorage: 50Mi
# Deviations from the hardening baseline, forced by gluetun's role as VPN
# gateway:
# - runs as root (no runAsNonRoot): required to create the tun device and
# program the firewall.
# - NET_ADMIN capability: creates the Wireguard tunnel and the iptables
# rules (firewall, kill switch).
# - readOnlyRootFilesystem: false: gluetun rewrites /etc/resolv.conf and
# other files on its root filesystem at runtime.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- NET_ADMIN
qbittorrent:
enabled: true
image:
repository: linuxserver/qbittorrent
tag: 5.2.1
# Every app gets its own Ingress object (named <release>-<chart>-<app>-ingress)
# with its own TLS secret tls-<release>-<chart>-<app>-ingress, so the apps can
# be exposed independently and under unrelated hosts.
# host is required while enabled is true and has no default - an app whose
# ingress is switched off never demands one. clusterIssuer, className and
# annotations may be set here to override the global ingress block.
# (Same block for prowlarr, radarr, sonarr, seerr and bazarr below.)
ingress:
enabled: true
host: null
# clusterIssuer: null
# className: null
# annotations: {}
storage:
size: 1Gi
# Network interface qBittorrent binds its BitTorrent session to - gluetun's
# Wireguard device. Without this binding libtorrent listens and announces on
# every interface in the shared pod network namespace, including eth0 (the
# pod IP) and loopback. Those announces bypass the tunnel, so gluetun's kill
# switch (OUTPUT DROP except tun0 and the allowed cluster subnets) drops
# them: trackers report "skipping tracker announce (unreachable)", no peers
# are found and torrents stall at 0 bytes in stalledDL. Binding to tun0 is
# therefore both the functional fix and a leak fix - it stops qBittorrent
# from listening on the pod IP past the tunnel. The value is written into
# /config/qBittorrent/qBittorrent.conf by the config-interface init
# container below; setting it to null or "" removes that init container and
# leaves the binding to the user (not recommended).
networkInterface: tun0
# The init container that seeds qbittorrent.networkInterface into the
# persisted qBittorrent config before the app starts. Runs as puid/pgid so
# the config file keeps the ownership the app expects.
configInit:
# Auxiliary busybox image. Pinned independently of appVersion (which
# tracks gluetun); reviewed alongside every app-version update round.
image:
repository: busybox
tag: "1.38.0"
resources:
limitFactor: 3
requests:
cpu: 10m
memory: 16Mi
ephemeralStorage: 1Mi
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
env: []
livenessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.2
memory: 512Mi
ephemeralStorage: 100Mi
# Deviations from the hardening baseline, forced by the linuxserver.io
# images (s6-overlay init that starts as root and drops to PUID/PGID):
# - no runAsNonRoot: s6-overlay must start as root to apply PUID/PGID and
# chown /config.
# - readOnlyRootFilesystem: false: the s6 init applies PUID/PGID by
# rewriting /etc/passwd and /etc/group and stages runtime state on the
# root filesystem. With a read-only root filesystem the containers still
# start, but silently keep the image's built-in uid/gid (911) instead of
# PUID/PGID (verified at runtime) - unacceptable for the shared media
# volumes.
# - capabilities: ALL dropped except what the s6-overlay init needs:
# CHOWN/FOWNER/DAC_OVERRIDE (taking ownership of /config and /run) and
# SETUID/SETGID (dropping from root to the PUID/PGID user).
# (Same block for all linuxserver.io containers: qbittorrent, prowlarr,
# radarr, sonarr, bazarr.)
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
prowlarr:
enabled: true
image:
repository: linuxserver/prowlarr
tag: 2.3.5
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
# clusterIssuer: null
# className: null
# annotations: {}
storage:
size: 1Gi
env: []
# The *arr apps expose /ping as an unauthenticated health endpoint.
livenessProbe:
httpGet:
path: /ping
port: 9696
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: 9696
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /ping
port: 9696
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
# linuxserver.io image - see the qbittorrent securityContext comment.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
flaresolverr:
enabled: true
image:
repository: ghcr.io/flaresolverr/flaresolverr
tag: v3.5.0
# No ingress and no config volume: flaresolverr is stateless and only
# consumed by prowlarr over localhost.
env: []
# /health is flaresolverr's dedicated lightweight health endpoint.
livenessProbe:
httpGet:
path: /health
port: 8191
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8191
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /health
port: 8191
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.2
memory: 512Mi
ephemeralStorage: 200Mi
# Deviations from the hardening baseline, forced by the flaresolverr image:
# - readOnlyRootFilesystem: false: on startup flaresolverr copies and
# patches the chromedriver binary to the hardcoded path /app/chromedriver
# and writes chromedriver caches under /app (its home directory).
# - runAsUser/runAsGroup pin the image's "flaresolverr" user numerically
# (uid/gid 1000; the image declares it by name only, which the kubelet's
# runAsNonRoot check cannot verify).
# Chromium's scratch paths (/tmp, /dev/shm) are emptyDirs.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
radarr:
enabled: true
image:
repository: linuxserver/radarr
tag: 6.1.1
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
# clusterIssuer: null
# className: null
# annotations: {}
storage:
size: 1Gi
env: []
livenessProbe:
httpGet:
path: /ping
port: 7878
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: 7878
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /ping
port: 7878
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
# linuxserver.io image - see the qbittorrent securityContext comment.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
recyclarr:
enabled: true
image:
repository: ghcr.io/recyclarr/recyclarr
tag: 8.6.0
# No ingress: recyclarr has no web UI, it syncs TRaSH guides to radarr and
# sonarr on a schedule (daemon mode).
storage:
size: 1Gi
env: []
# recyclarr serves no HTTP port in daemon mode, so all probes exec the CLI.
livenessProbe:
exec:
command:
- recyclarr
- --version
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command:
- recyclarr
- --version
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
exec:
command:
- recyclarr
- --version
periodSeconds: 5
timeoutSeconds: 10
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.05
memory: 128Mi
ephemeralStorage: 50Mi
# The image supports running as an arbitrary non-root user; runAsUser and
# runAsGroup are set from the global puid/pgid values in the template - do
# not set them here.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
sonarr:
enabled: true
image:
repository: linuxserver/sonarr
tag: 4.0.17
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
# clusterIssuer: null
# className: null
# annotations: {}
storage:
size: 1Gi
env: []
livenessProbe:
httpGet:
path: /ping
port: 8989
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: 8989
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /ping
port: 8989
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
# linuxserver.io image - see the qbittorrent securityContext comment.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
seerr:
enabled: true
image:
repository: ghcr.io/seerr-team/seerr
tag: v3.3.0
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
# clusterIssuer: null
# className: null
# annotations: {}
storage:
size: 1Gi
env: []
# /api/v1/settings/public answers unauthenticated once the app is up.
livenessProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. First boot runs database
# migrations: budget 10s * 30 = 300s.
startupProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
# The image runs fine as an arbitrary non-root user; runAsUser and
# runAsGroup are set from the global puid/pgid values in the template - do
# not set them here.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
bazarr:
enabled: true
image:
repository: linuxserver/bazarr
tag: 1.5.6
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
# clusterIssuer: null
# className: null
# annotations: {}
storage:
size: 1Gi
env: []
# bazarr has no unauthenticated health endpoint; / serves the UI with 200.
livenessProbe:
httpGet:
path: /
port: 6767
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 6767
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /
port: 6767
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
# linuxserver.io image - see the qbittorrent securityContext comment.
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID