qBittorrent had no network interface configured, so libtorrent listened and announced on every interface of the shared pod network namespace - including eth0 (the pod IP) and loopback. gluetun's kill switch drops exactly those packets, so trackers answered with "skipping tracker announce (unreachable)" or timed out, no peers were found and torrents stalled at 0 bytes in stalledDL. Beyond the stall this is a leak: without the binding qBittorrent also listens on the pod IP, i.e. reachable past the tunnel. The setting only exists in the persisted config file, which qBittorrent creates itself on first start, so an init container seeds it before the app comes up. The edit is idempotent and preserves existing user settings: it replaces the keys if present, inserts them into the [BitTorrent] section if missing, and creates file or section if needed. The interface is configurable via qbittorrent.networkInterface (default tun0); emptying it drops the init container.
624 lines
18 KiB
YAML
624 lines
18 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
|
|
media:
|
|
downloads:
|
|
existingClaim: null
|
|
movies:
|
|
existingClaim: null
|
|
tv:
|
|
existingClaim: null
|
|
|
|
ingress:
|
|
enabled: true
|
|
clusterIssuer: null
|
|
domain: null
|
|
className: nginx
|
|
# Extra annotations for the Ingress; 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
|
|
ingress:
|
|
enabled: true
|
|
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
|
|
ingress:
|
|
enabled: true
|
|
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
|
|
ingress:
|
|
enabled: true
|
|
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
|
|
ingress:
|
|
enabled: true
|
|
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
|
|
ingress:
|
|
enabled: true
|
|
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
|
|
ingress:
|
|
enabled: true
|
|
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
|