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.
866 lines
31 KiB
YAML
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
|