Files
helm-charts/freesailarr/templates/freesailarr.sfs.yaml
T
jklein 5e2d8e4083 Allow writable root filesystem for the linuxserver.io containers
With readOnlyRootFilesystem the s6-overlay init cannot rewrite
/etc/passwd, so the containers silently kept the image's built-in
uid/gid 911 instead of the configured PUID/PGID (verified at runtime) -
breaking ownership on the shared media volumes. Drop the now-unneeded
/run and /tmp emptyDirs; deviation documented in values.yaml.
2026-08-24 13:41:47 +02:00

420 lines
18 KiB
YAML

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec:
replicas: {{ include "freesailarr.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) }}
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
template:
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec:
automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.securityContext.pod | nindent 8 }}
containers:
# gluetun is the VPN gateway: all app containers share the pod's
# network namespace, talk to each other over localhost and reach the
# internet only through the Wireguard tunnel (gluetun's firewall acts
# as kill switch).
- name: gluetun
image: "qmcgaw/gluetun:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
# An uncleanly exited gluetun (crash, OOM killer, node issue) leaves
# its ip rule, routing table and tun device behind in the shared pod
# network namespace, and the restarted container then fails forever
# ("adding ip rule 101 ... file exists" / silent tunnel breakage)
# while the kill switch blocks all egress. The upstream-recommended
# postStart hook is racy (it runs concurrently with the entrypoint
# and can delete the rule the new instance just created), so the
# stale state is cleaned synchronously before gluetun starts.
command:
- /bin/sh
- -c
- 'while ip rule del table 51820 2>/dev/null; do :; done; while ip -6 rule del table 51820 2>/dev/null; do :; done; ip link del tun0 2>/dev/null; exec /gluetun-entrypoint'
env:
- name: TZ
value: {{ .Values.timezone | quote }}
- name: VPN_SERVICE_PROVIDER
value: {{ .Values.gluetun.vpn.serviceProvider | quote }}
- name: VPN_TYPE
value: {{ .Values.gluetun.vpn.type | quote }}
- name: WIREGUARD_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-{{ .Chart.Name }}-gluetun-secret
key: wireguard-private-key
- name: WIREGUARD_ADDRESSES
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-{{ .Chart.Name }}-gluetun-secret
key: wireguard-addresses
{{- with .Values.gluetun.vpn.serverCities }}
- name: SERVER_CITIES
value: {{ . | quote }}
{{- end }}
{{- with .Values.gluetun.vpn.serverCountries }}
- name: SERVER_COUNTRIES
value: {{ . | quote }}
{{- end }}
{{- with .Values.gluetun.vpn.dnsUpstreamResolvers }}
- name: DNS_UPSTREAM_RESOLVERS
value: {{ . | quote }}
{{- end }}
{{- with .Values.gluetun.vpn.firewallOutboundSubnets }}
- name: FIREWALL_OUTBOUND_SUBNETS
value: {{ . | quote }}
{{- end }}
# Open the firewall for the UI/probe ports of the enabled apps and
# the health server - kubelet probes and ingress traffic arrive on
# the pod IP and would be dropped otherwise.
- name: FIREWALL_INPUT_PORTS
value: {{ include "freesailarr.firewallInputPorts" . | quote }}
# Bind the health server to all interfaces so the kubelet HTTP
# probes (which connect to the pod IP, not localhost) reach it.
- name: HEALTH_SERVER_ADDRESS
value: ":9999"
{{- include "freesailarr.env" (dict "root" $ "env" .Values.gluetun.env) | nindent 12 }}
ports:
- containerPort: 9999
protocol: TCP
name: health
livenessProbe:
{{- toYaml .Values.gluetun.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.gluetun.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.gluetun.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.gluetun.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.gluetun.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /gluetun
name: gluetun-config
- mountPath: /dev/net/tun
name: dev-net-tun
{{- if .Values.qbittorrent.enabled }}
- name: qbittorrent
image: "{{ .Values.qbittorrent.image.repository }}:{{ .Values.qbittorrent.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: {{ .Values.puid | quote }}
- name: PGID
value: {{ .Values.pgid | quote }}
- name: TZ
value: {{ .Values.timezone | quote }}
- name: WEBUI_PORT
value: "8080"
{{- include "freesailarr.env" (dict "root" $ "env" .Values.qbittorrent.env) | nindent 12 }}
ports:
- containerPort: 8080
protocol: TCP
name: qbittorrent
livenessProbe:
{{- toYaml .Values.qbittorrent.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.qbittorrent.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.qbittorrent.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.qbittorrent.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.qbittorrent.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /config
name: qbittorrent-config
- mountPath: /downloads
name: media-downloads
{{- end }}
{{- if .Values.prowlarr.enabled }}
- name: prowlarr
image: "{{ .Values.prowlarr.image.repository }}:{{ .Values.prowlarr.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: {{ .Values.puid | quote }}
- name: PGID
value: {{ .Values.pgid | quote }}
- name: TZ
value: {{ .Values.timezone | quote }}
{{- include "freesailarr.env" (dict "root" $ "env" .Values.prowlarr.env) | nindent 12 }}
ports:
- containerPort: 9696
protocol: TCP
name: prowlarr
livenessProbe:
{{- toYaml .Values.prowlarr.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.prowlarr.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.prowlarr.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.prowlarr.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.prowlarr.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /config
name: prowlarr-config
{{- end }}
{{- if .Values.flaresolverr.enabled }}
- name: flaresolverr
image: "{{ .Values.flaresolverr.image.repository }}:{{ .Values.flaresolverr.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: {{ .Values.timezone | quote }}
{{- include "freesailarr.env" (dict "root" $ "env" .Values.flaresolverr.env) | nindent 12 }}
ports:
- containerPort: 8191
protocol: TCP
name: flaresolverr
livenessProbe:
{{- toYaml .Values.flaresolverr.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.flaresolverr.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.flaresolverr.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.flaresolverr.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.flaresolverr.securityContext | nindent 12 }}
volumeMounts:
# Writable paths for headless chromium so the root filesystem
# stays read-only; /dev/shm is a memory-backed emptyDir because
# chromium needs more shared memory than the container default.
- mountPath: /tmp
name: flaresolverr-tmp
- mountPath: /dev/shm
name: flaresolverr-shm
{{- end }}
{{- if .Values.radarr.enabled }}
- name: radarr
image: "{{ .Values.radarr.image.repository }}:{{ .Values.radarr.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: {{ .Values.puid | quote }}
- name: PGID
value: {{ .Values.pgid | quote }}
- name: TZ
value: {{ .Values.timezone | quote }}
{{- include "freesailarr.env" (dict "root" $ "env" .Values.radarr.env) | nindent 12 }}
ports:
- containerPort: 7878
protocol: TCP
name: radarr
livenessProbe:
{{- toYaml .Values.radarr.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.radarr.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.radarr.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.radarr.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.radarr.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /config
name: radarr-config
- mountPath: /movies
name: media-movies
- mountPath: /downloads
name: media-downloads
{{- end }}
{{- if .Values.recyclarr.enabled }}
- name: recyclarr
image: "{{ .Values.recyclarr.image.repository }}:{{ .Values.recyclarr.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: {{ .Values.timezone | quote }}
- name: RECYCLARR_CREATE_CONFIG
value: "true"
{{- include "freesailarr.env" (dict "root" $ "env" .Values.recyclarr.env) | nindent 12 }}
livenessProbe:
{{- toYaml .Values.recyclarr.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.recyclarr.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.recyclarr.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.recyclarr.resources | nindent 12 }}
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
{{- toYaml .Values.recyclarr.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /config
name: recyclarr-config
# Writable scratch space for the .NET runtime so the root
# filesystem stays read-only.
- mountPath: /tmp
name: recyclarr-tmp
{{- end }}
{{- if .Values.sonarr.enabled }}
- name: sonarr
image: "{{ .Values.sonarr.image.repository }}:{{ .Values.sonarr.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: {{ .Values.puid | quote }}
- name: PGID
value: {{ .Values.pgid | quote }}
- name: TZ
value: {{ .Values.timezone | quote }}
{{- include "freesailarr.env" (dict "root" $ "env" .Values.sonarr.env) | nindent 12 }}
ports:
- containerPort: 8989
protocol: TCP
name: sonarr
livenessProbe:
{{- toYaml .Values.sonarr.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.sonarr.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.sonarr.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.sonarr.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.sonarr.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /config
name: sonarr-config
- mountPath: /tv
name: media-tv
- mountPath: /downloads
name: media-downloads
{{- end }}
{{- if .Values.seerr.enabled }}
- name: seerr
image: "{{ .Values.seerr.image.repository }}:{{ .Values.seerr.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: {{ .Values.timezone | quote }}
{{- include "freesailarr.env" (dict "root" $ "env" .Values.seerr.env) | nindent 12 }}
ports:
- containerPort: 5055
protocol: TCP
name: seerr
livenessProbe:
{{- toYaml .Values.seerr.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.seerr.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.seerr.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.seerr.resources | nindent 12 }}
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
{{- toYaml .Values.seerr.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /app/config
name: seerr-config
- mountPath: /tmp
name: seerr-tmp
{{- end }}
{{- if .Values.bazarr.enabled }}
- name: bazarr
image: "{{ .Values.bazarr.image.repository }}:{{ .Values.bazarr.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: {{ .Values.puid | quote }}
- name: PGID
value: {{ .Values.pgid | quote }}
- name: TZ
value: {{ .Values.timezone | quote }}
{{- include "freesailarr.env" (dict "root" $ "env" .Values.bazarr.env) | nindent 12 }}
ports:
- containerPort: 6767
protocol: TCP
name: bazarr
livenessProbe:
{{- toYaml .Values.bazarr.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.bazarr.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.bazarr.startupProbe | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.bazarr.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.bazarr.securityContext | nindent 12 }}
volumeMounts:
- mountPath: /config
name: bazarr-config
- mountPath: /movies
name: media-movies
- mountPath: /tv
name: media-tv
{{- end }}
restartPolicy: Always
volumes:
# The tun device gluetun needs to create the Wireguard tunnel.
- name: dev-net-tun
hostPath:
path: /dev/net/tun
type: CharDevice
{{- if or .Values.qbittorrent.enabled .Values.radarr.enabled .Values.sonarr.enabled }}
- name: media-downloads
persistentVolumeClaim:
claimName: {{ required "media.downloads.existingClaim is required when qbittorrent, radarr or sonarr is enabled" .Values.media.downloads.existingClaim }}
{{- end }}
{{- if or .Values.radarr.enabled .Values.bazarr.enabled }}
- name: media-movies
persistentVolumeClaim:
claimName: {{ required "media.movies.existingClaim is required when radarr or bazarr is enabled" .Values.media.movies.existingClaim }}
{{- end }}
{{- if or .Values.sonarr.enabled .Values.bazarr.enabled }}
- name: media-tv
persistentVolumeClaim:
claimName: {{ required "media.tv.existingClaim is required when sonarr or bazarr is enabled" .Values.media.tv.existingClaim }}
{{- end }}
{{- if .Values.flaresolverr.enabled }}
- name: flaresolverr-tmp
emptyDir: {}
- name: flaresolverr-shm
emptyDir:
medium: Memory
sizeLimit: 512Mi
{{- end }}
{{- if .Values.recyclarr.enabled }}
- name: recyclarr-tmp
emptyDir: {}
{{- end }}
{{- if .Values.seerr.enabled }}
- name: seerr-tmp
emptyDir: {}
{{- end }}
# Config volumes for gluetun and the enabled apps. NOTE: volumeClaimTemplates
# are immutable - toggling an app's enabled flag later requires recreating
# the StatefulSet once (kubectl delete statefulset <name> --cascade=orphan
# before upgrading; existing PVCs and their data survive).
volumeClaimTemplates:
{{- range $app := list "gluetun" "qbittorrent" "prowlarr" "radarr" "recyclarr" "sonarr" "seerr" "bazarr" }}
{{- $config := index $.Values $app }}
{{- if or (eq $app "gluetun") $config.enabled }}
- metadata:
name: {{ $app }}-config
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ $config.storage.size }}
{{- with $.Values.storage.storageClass }}
storageClassName: {{ . }}
{{- end }}
{{- end }}
{{- end }}