Files
helm-charts/freesailarr/values.yaml
T
jklein fc768efaf5 freesailarr: shared domain with sub-paths, qBittorrent save path (3.0.0+up1.0.0)
Adds a second, optional ingress shape and fixes the download target of
qBittorrent. The existing per-app-host shape stays the default and renders
unchanged.

Shared domain (#167)

Setting ingress.host moves every app without a host of its own into one
Ingress object with one path each (<app>.ingress.path, default /<app>), one
TLS secret and therefore one DNS name and one certificate for the whole
stack. A per-app host always wins and keeps its own object, so mixed
operation needs no extra values.

The apps differ in what they can do under a sub-path, and the chart follows
that rather than pretending they are alike:

  - radarr, sonarr, prowlarr get <APP>__SERVER__URLBASE from the chart, but
    only when their own env block does not already carry the variable.
  - bazarr has no environment variable for its base path and stays one
    documented manual step (Settings -> General -> Base URL).
  - qbittorrent has no base path at all and renders a second object on the
    same host with a prefix-stripping rewrite. It repeats the same TLS secret
    name and deliberately omits the cert-manager annotation, so both objects
    state their own TLS while only one certificate is requested.
  - seerr has no base path either and cannot be rewritten, so it is pinned to
    "/" and a fail refuses any other path. A second fail refuses two apps
    claiming the same path.

qBittorrent save path (#166)

Without Session\DefaultSavePath qBittorrent writes to its built-in default
/config/Downloads - the 1Gi config volume. It fills within minutes, every
torrent goes to "errored" and qBittorrent can no longer write its own
metadata to BT_backup, so a restart in that state costs the torrent list.

The init container (renamed qbittorrent-config-interface ->
qbittorrent-config-seed, since it no longer only sets the interface) now
seeds qbittorrent.savePath, default /downloads. Unlike the interface binding
it is inserted ONLY when the key is absent and never replaced: a path changed
in the WebUI is a legitimate operator decision and must survive a restart.

This is the reason for the major bump: an installation that never set the
path writes somewhere else after the upgrade.

Documentation

NOTES.txt and the media block in values.yaml now spell out why a shared claim
with three subPaths does not produce hardlinks: the kernel compares the
vfsmount in do_linkat, not the superblock, so two bind mounts of one
filesystem are EXDEV - while stat reports the same device number on both
sides and suggests the opposite. The layout that would work (one claim, one
mount, categories as sub-directories) is named as a possibility; the chart
cannot express it today.

README.md gains the shared domain as the second admissible ingress shape of
the style guideline.
2026-08-25 20:55:43 +02:00

866 lines
31 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):
# NO image tag comes from appVersion. All nine images, gluetun included, are
# pinned here via <app>.image.repository / <app>.image.tag, because the stack
# combines nine independently versioned upstream projects and none of them is
# "the" one app version.
#
# That is also what puts every image under Renovate: the built-in helm-values
# manager reads image.repository/image.tag pairs out of this file and proposes
# one update per image, and ci/renovate-bump-chart.sh bumps the chart version
# along with it. A chart whose main image came from appVersion would instead
# need a hard-coded block in renovate.json5 - freesailarr deliberately has none.
#
# Chart.yaml's appVersion is therefore the chart's own version marker, freely
# chosen and tied to no upstream - not the source of any tag here. Nothing
# rewrites it automatically; raise it by hand when you want to mark a new
# edition of the chart as a whole.
# 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.
# How far that reaches is the CSI driver's decision, not this chart's, and the
# media claims are the case where it stops short: a driver whose fsGroupPolicy
# is ReadWriteOnceWithFSType - Longhorn's setting - applies fsGroup to RWO
# volumes only, so on an RWX media claim fsGroup is ignored outright and
# OnRootMismatch never even gets a say. The config claims are RWO and are
# covered normally. Consequence for the media claims: their root directory has
# to be writable for PUID/PGID by itself, see the media block below (#164).
# seccompProfile RuntimeDefault applies the container runtime's default
# syscall filter to every container in the pod, init containers included, and
# is set at pod level so a container added later inherits it without the
# value being repeated. It is worth naming what it has to survive here:
# gluetun runs as root with NET_ADMIN, creates a tun device and programs
# iptables, and the s6-overlay init of the five linuxserver.io images starts
# as root and drops to PUID/PGID via CHOWN/SETUID/SETGID. Both were verified
# against a real tunnel under this filter - see the chart's PR.
# A container that genuinely needs a different filter can still override it
# under <app>.securityContext, which wins over the pod value.
pod:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
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 back two
# or all three categories, which is what makes the one-library-volume layout
# possible - one claim, one sub-directory per category:
#
# media:
# downloads: { existingClaim: media-library, subPath: downloads }
# movies: { existingClaim: media-library, subPath: movies }
# tv: { existingClaim: media-library, subPath: tv-shows }
#
# Categories sharing a claim get exactly ONE pod volume entry and differ only in
# the subPath of their mounts (#164). That is not cosmetic: a pod that lists the
# same claim twice never starts - it stays in Init:0/1 forever without a single
# kubelet event (measured on RKE2/Longhorn, with RWO as well as RWX). The entry
# is named after the first ENABLED category in the fixed order downloads,
# movies, tv that carries the claim, so the name does not wander when an
# unrelated app flag changes (see the mediaVolumeName helper).
#
# The subPath directories do NOT have to exist beforehand - the kubelet creates
# a missing one. What it does not do is give it usable permissions: the created
# directory belongs to root and inherits the mode of the CLAIM ROOT. So the one
# thing that has to hold is:
#
# the root directory of the claim must be writable for PUID/PGID.
#
# Do not count on fsGroup for that. Whether the kubelet applies fsGroup to a
# volume at all is the CSI driver's decision (its fsGroupPolicy), not the pod's:
# Longhorn ships ReadWriteOnceWithFSType, so fsGroup applies to its RWO volumes
# and is silently ignored on an RWX claim - measured by the reporter of #164 on
# the target cluster (kubectl get csidriver driver.longhorn.io -o
# jsonpath='{.spec.fsGroupPolicy}').
#
# In practice this works out on Longhorn because Longhorn hands out its volume
# root as mode 777, and a subPath directory created under it inherits those 777
# - root-owned, but writable for everyone. That is a property of the driver, not
# of Kubernetes. Measured in k3d against a claim whose root is 0755 and with no
# fsGroup in effect: the kubelet creates /movies as root:root 0755 and the
# container's `touch` fails with EPERM. On such a driver, prepare the claim root
# once (chown 1000:1000 or chmod g+w with the right group) before the first
# start - the chart deliberately does not chown anything: a multi-TB library
# must not be walked on every pod start.
#
# What this buys and what it does NOT buy - measured in a k3d cluster against
# this chart, because the usual argument for a shared volume is hardlinking:
# - It buys the layout: one claim to provision, back up and share with
# Jellyfin, with the categories as sibling directories instead of three
# separate volumes.
# - It does NOT buy hardlinked imports. Radarr and Sonarr import by moving,
# and a move within one filesystem can be a hardlink or a rename instead of
# a copy - but Kubernetes turns every volumeMount into its own bind mount,
# and link()/rename() across two mount points fails with EXDEV even when
# both sides are the same claim on the same device. Measured: /downloads and
# /movies in the radarr container both report device 65025 and resolve to
# the same PV directory, yet `ln /downloads/f /movies/f` fails with
# "Cross-device link", while the identical link succeeds when the claim root
# is mounted once and both categories are sub-directories under it.
# This is not a property of subPath: two plain mounts of the same claim
# without any subPath fail exactly the same way, so the chart has never
# supported cross-category hardlinks.
# - Consequence today: an import still copies, so download and library each
# hold a full copy until the torrent is removed.
#
# HARDLINKS NEED ONE MOUNT, NOT ONE CLAIM. This is the conclusion people draw
# from the paragraph above and get wrong, so it is spelled out: adding downloads
# as a third subPath on the shared claim changes NOTHING about hardlinks. The
# kernel compares the VFSMOUNT in do_linkat, not the superblock, and two bind
# mounts of the same filesystem are two vfsmounts - hence EXDEV. Measured again
# on Longhorn/NFS: both mount points report st_dev 0:56, the mount table shows
# two mount IDs on that one device, a cross-mount link() fails, and a link()
# inside a single mount succeeds with link count 2.
# The device number is the trap. "stat" reports the same device on both sides
# and thereby suggests the exact opposite of what happens - which is why this
# reads like an application bug instead of a property of the mount namespace.
# The layout that WOULD hardlink is one claim with exactly ONE mount and the
# categories as sub-directories inside it (the "one /data mount" layout the *arr
# guides recommend). The chart cannot express that today - it mounts each
# category separately. Named as a possibility, not as instructions: there are no
# values for it, and providing them would be a change to the mount layout.
media:
downloads:
existingClaim: null
subPath: null
movies:
existingClaim: null
subPath: null
tv:
existingClaim: null
subPath: null
# Ingress defaults. There are two shapes, and they mix freely:
#
# 1. One host per app (the default). Every app carries its own
# <app>.ingress.host, so the hosts are independent of each other and may
# live in completely different domains. Each app gets its own Ingress
# object and its own certificate.
# 2. One shared domain with sub-paths: set ingress.host and leave the per-app
# hosts empty. Every app then moves into ONE Ingress object as a sub-path
# (<app>.ingress.path, default /<app>), which means one DNS name and one
# certificate for the whole stack instead of six.
#
# A per-app host always wins: an app that has one keeps its own object even
# while ingress.host is set, so a mixed setup needs no extra values.
#
# 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). The shared object uses the global values only -
# it belongs to no single app.
#
# What the sub-path mode costs, per app, measured against the upstream
# documentation and not assumed:
#
# - radarr, sonarr, prowlarr: nothing. The chart sets their UrlBase itself
# (RADARR__SERVER__URLBASE and friends) as soon as they render under a
# path other than "/" - unless that app's own env block already contains
# the variable, in which case yours wins and the chart stays out.
# - bazarr: ONE manual step. Bazarr reads its base path only from its own
# configuration, it has no environment variable for it, so after the first
# start set Settings -> General -> Base URL to the same value as
# bazarr.ingress.path and restart the pod. Until then bazarr answers only
# at the root and the sub-path yields a broken UI. Deliberately not
# automated: seeding it would mean an init container writing into bazarr's
# config database, which is a much bigger promise than this chart wants to
# make. Note that bazarr's probes address "/" - if it does not answer
# there once a base URL is set, adjust bazarr.livenessProbe/readinessProbe/
# startupProbe accordingly (they are values, precisely for cases like this).
# - qbittorrent: it has no base path at all, so the chart gives it its own
# Ingress object on the same host with an nginx rewrite that strips the
# prefix. Two traps from the upstream recipe, both of which look like a
# login failure and therefore point in the wrong direction:
# * qBittorrent marks its session cookie "Secure". Reaching the UI over
# plain HTTP means the browser never sends the cookie back and every
# click bounces to the login form. Use https.
# * qBittorrent bans the client IP for a while after a few failed login
# attempts. Once that has triggered, even correct credentials fail -
# the ban has to expire (or the pod be restarted) first.
# If it does not work behind the stripped prefix, the fallback costs no
# chart change: set qbittorrent.ingress.host and qBittorrent is back on a
# host of its own while everything else stays on the shared domain.
# - seerr: it has no base-path support either and, unlike qBittorrent,
# cannot be fixed by a rewrite (the upstream request has been open since
# May 2022, https://github.com/Fallenbagel/jellyseerr/issues/97). It
# therefore sits at "/" and the chart refuses to render it anywhere else.
# If seerr is switched off, nothing serves the root and the shared domain
# answers 404 there. That is correct and left alone on purpose.
ingress:
# Master switch: false suppresses every app ingress regardless of the
# per-app toggles.
enabled: true
# The shared domain for the sub-path mode, e.g. "media.example.com". Empty
# means the default: each app on a host of its own.
host: ""
clusterIssuer: null
className: nginx
# Extra annotations for the Ingress objects; merged with the cert-manager
# cluster-issuer annotation the chart always sets.
annotations: {}
gluetun:
# Pinned here like every other image in this chart, so Renovate's helm-values
# manager proposes gluetun updates too. Docker Hub tags carry a v prefix.
image:
repository: docker.io/qmcgaw/gluetun
tag: v3.41.3
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: docker.io/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, and neither does an app served
# under the shared ingress.host. clusterIssuer, className and annotations may
# be set here to override the global ingress block.
# path is only read in the shared-domain mode (see the ingress block above);
# it defaults to /<app> and is ignored while this app has a host of its own.
# (Same block for prowlarr, radarr, sonarr, seerr and bazarr below.)
ingress:
enabled: true
host: null
path: /qbittorrent
# clusterIssuer: null
# className: null
# annotations: {}
storage:
# The CONFIG volume, and it is meant to stay small: it holds
# qBittorrent.conf, the BT_backup state and nothing else. Downloads must
# never land here - see savePath below for what happens when they do.
size: 1Gi
# Default save path for new torrents, seeded into the persisted config by the
# init container below. Without it qBittorrent falls back to its built-in
# default /config/Downloads, which is this 1Gi config volume: it fills within
# minutes, every torrent goes to "errored", and qBittorrent can no longer
# even write its own metadata to BT_backup - so a restart in that state costs
# the torrent list. The failure looks like a network, tracker or VPN problem
# and needs a "df" to recognise. Measured on a real cluster, not assumed.
# /downloads is the media claim (media.downloads), mounted into this
# container.
# Unlike networkInterface below this value is seeded ONLY when the key is
# absent from the config, never replaced: the save path is a legitimate
# operator decision, and someone who changes it in the WebUI must not be
# overwritten on every pod restart. Set it to null or "" to opt out entirely
# and leave the path to qBittorrent's built-in default (not recommended, see
# above).
savePath: /downloads
# 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-seed init container
# below, and it is ENFORCED on every start (unlike savePath), because a
# deviating value would be a leak; setting it to null or "" drops the binding
# and leaves it to the user (not recommended).
networkInterface: tun0
# The init container that seeds qbittorrent.networkInterface and
# qbittorrent.savePath into the persisted qBittorrent config before the app
# starts. Runs as puid/pgid so the config file keeps the ownership the app
# expects. Setting both networkInterface and savePath to null removes the
# init container entirely.
configInit:
# Auxiliary busybox image for the init container, pinned here like every
# other image in this chart and proposed by Renovate the same way.
image:
repository: docker.io/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: docker.io/linuxserver/prowlarr
tag: 2.3.5
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
path: /prowlarr
# 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: docker.io/linuxserver/radarr
tag: 6.1.1
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
path: /radarr
# 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: docker.io/linuxserver/sonarr
tag: 4.0.17
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
path: /sonarr
# 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
path: /
# 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: docker.io/linuxserver/bazarr
tag: 1.5.6
# See the qbittorrent ingress comment.
ingress:
enabled: true
host: null
path: /bazarr
# 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