MediathekArr fakes a Usenet indexer towards Prowlarr and a SABnzbd downloader towards Sonarr/Radarr and fetches from the ARD and ZDF media libraries. Both consumers already share gluetun's network namespace, so they reach it over 127.0.0.1 - no service, no DNS, no ingress detour. Purely additive: the flag defaults to false and a default render is byte-identical to 3.0.5 (verified against the previous chart for both the default values and the CI values). The four things that fail silently without them: - A START GATE on gluetun. MediathekArr fetches its ruleset once at start and never retries; started before the tunnel it answers every Newznab search with 0 results while the container is up, Ready and quiet. Kubernetes cannot order two regular containers and an initContainer would deadlock, so the wait sits in front of the image entrypoint: poll gluetun's health server on 127.0.0.1:9999 and exec the entrypoint afterwards; on timeout the container exits rather than starting blind, because a crash loop is visible and an empty indexer is not. The startup probe budget (300s) deliberately outlasts the gate (180s). - incomplete/ AND complete/ ON THE SHARED DOWNLOADS CLAIM, not in an emptyDir. An emptyDir counts against the container's ephemeral-storage limit, and one large download would have the kubelet evict the whole pod with all ten containers - the trap ytdlp-web-player documents. - THE COMPLETE PATH SONARR SEES. The claim is mounted at /downloads exactly as sonarr/radarr mount it, and the chart sets DOWNLOAD_COMPLETE_PATH itself, so the SABnzbd complete_dir it reports is the same absolute path on both sides. - TWO PORTS: 5007 (SABnzbd role and UI, primary) and 5008 (Newznab role for Prowlarr), both declared to gluetun's firewall and both on one Service. The SABnzbd API is at /download/api - /api on 5007 answers with a misleading Newznab validation error. Further details: - Image pinned by digest (tag + digest): "latest" is a 2024 build, "beta" moves. Single-arch linux/amd64. The built-in helm-values manager cannot follow that pair, so renovate.json5 gets the chart's first customManagers block; verified by dry-run with two fixtures, since a matching and a broken rule look identical while the pin is current. - Hardened as measured: non-root, uid/gid from puid/pgid, readOnlyRootFilesystem: true, all capabilities dropped, no PUID/PGID. - Probes on measured 200 endpoints (there is no health endpoint): readiness and startup on /download/, liveness as an exec covering BOTH ports, because one container runs two processes and a wedged Newznab half is exactly what makes searches come back empty. - No ingress by default: the UI links its assets with root-absolute paths, so it can only be served at the root of a host; on the shared domain that root belongs to seerr. A sub-path fails the render instead of producing a broken UI. - values.yaml and NOTES.txt document the geo MARKER PROCEDURE (not a table of series), the escape route of a German exit and its price for the whole stack, the fail_message: null trap, and the HLS question as an open question. - CI: mediathekarr enabled in ci/freesailarr/test-values.yaml, install timeout raised to 1200s for the extra image and the gate. Fixes #245
1134 lines
46 KiB
YAML
1134 lines
46 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 ten images, gluetun included, are
|
|
# pinned here via <app>.image.repository / <app>.image.tag, because the stack
|
|
# combines ten independently versioned upstream projects and none of them is
|
|
# "the" one app version. mediathekarr additionally carries an image.digest -
|
|
# see the reasoning in its own block at the end of this file.
|
|
#
|
|
# 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, mediathekarr
|
|
# 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.
|
|
# One measured detail, in case a probe ever looks odd: with a UrlBase set,
|
|
# these apps answer the bare /ping with "307 Temporary Redirect,
|
|
# Location: /<urlbase>/ping" instead of 200. The probes keep passing -
|
|
# the kubelet counts every status from 200 to 399 as success, and the
|
|
# containers come up Ready with 0 restarts (verified in k3d) - so the
|
|
# probe paths are deliberately left at /ping and stay valid in both
|
|
# ingress shapes. Worth knowing before reading anything into that 307.
|
|
# - 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 the sub-path yields
|
|
# a broken UI - and it does NOT announce itself with a 404: bazarr answers
|
|
# every unknown path with its index page, so /bazarr/ AND /bazarr/<asset>
|
|
# both return "200 text/html" of identical length (measured in k3d). The
|
|
# page loads, its assets silently come back as HTML, and nothing looks
|
|
# like an error. Check the content type, not the status code. 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.3
|
|
# 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.5.2
|
|
# 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.3.0
|
|
# 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.7.1
|
|
# 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.19
|
|
# 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.4.1
|
|
# 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.6.0
|
|
# 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
|
|
|
|
# MediathekArr: the tenth container, and the only one that is OFF by default. It fakes a Usenet indexer towards Prowlarr and a SABnzbd downloader
|
|
# towards Sonarr/Radarr, and fetches the content over plain HTTP from the ARD
|
|
# and ZDF media libraries. Both consumers already sit in this pod and share
|
|
# gluetun's network namespace, so they reach it over 127.0.0.1 - no service, no
|
|
# DNS, no ingress detour.
|
|
#
|
|
# Off by default because it is additive and nobody's existing release should
|
|
# grow a container on an upgrade: enabling it creates a new
|
|
# volumeClaimTemplate, which a StatefulSet cannot take without being recreated
|
|
# (see the NOTES and the volumeClaimTemplates comment in the StatefulSet).
|
|
#
|
|
# ----------------------------------------------------------------------------
|
|
# THE GEO CAVEAT, AND HOW TO CHECK IT YOURSELF
|
|
# ----------------------------------------------------------------------------
|
|
# The tunnel leaves through Zurich by this chart's default, and ARD and ZDF
|
|
# block parts of their catalogue outside Germany. Every container hangs in
|
|
# gluetun's namespace; there is no bypass for a single container.
|
|
#
|
|
# Measured, 480 URLs across 12 channels: 104 of 473 loadable URLs were blocked
|
|
# through Zurich (22.0%), none of them on the German side. The spread is very
|
|
# uneven - ONE 97.5%, ZDFneo 47.5%, ZDFinfo 37.5%, Das Erste 20.5%, ZDF 10.3%,
|
|
# BR 0% - so a single number tells you little about the show you actually want.
|
|
#
|
|
# THE PROCEDURE, NOT A TABLE OF SERIES. A list of titles is stale in half a
|
|
# year; the marker in the CDN path is not. Over two measurement rounds it
|
|
# predicted the outcome perfectly - 584 of 584 cases in the full survey, not a
|
|
# single misclassification. Look at the media URL of an entry and read the
|
|
# path:
|
|
#
|
|
# /de/, /geo/de/, /deutschlandweit/, geoavailability_D -> blocked outside DE
|
|
# /weltweit/ -> open
|
|
# /none/ (ZDF) -> open
|
|
# /dach/, deChAt (WDR variant) -> open, Switzerland
|
|
# is part of DACH
|
|
#
|
|
# The obvious rule of thumb "own production = available worldwide" is MEASURABLY
|
|
# WRONG and is named here so nobody re-invents it: an ARD own production was
|
|
# blocked, a licensed series was open. Only the marker counts.
|
|
#
|
|
# THE ESCAPE ROUTE AND ITS PRICE. The exit is a deployment decision, not a chart
|
|
# property: set gluetun.vpn.serverCountries/serverCities to a German exit and
|
|
# the blocks disappear. The price is that this moves the WHOLE stack, all ten
|
|
# containers - qBittorrent included - into that jurisdiction and onto those
|
|
# servers. There is no per-container exit, and this chart deliberately does not
|
|
# pretend otherwise.
|
|
#
|
|
# ----------------------------------------------------------------------------
|
|
# THE fail_message: null TRAP - READ THIS BEFORE DEBUGGING A FAILED DOWNLOAD
|
|
# ----------------------------------------------------------------------------
|
|
# MediathekArr never checks the HTTP status code. Measured A/B with an identical
|
|
# NZB: direct (DE) -> status Completed, 94,235,525 bytes, fail_message null;
|
|
# through Zurich -> status Failed, 0 bytes, fail_message ALSO null.
|
|
#
|
|
# On a geo-blocked fetch it writes the ~476-byte HTML error page out as .mp4 and
|
|
# logs "Download completed". Only mkvmerge then refuses the file (exit code 2)
|
|
# and flips the status to Failed. Two consequences:
|
|
#
|
|
# 1. Sonarr is told "failed" without a reason. A geo block looks exactly like
|
|
# a broken server, an expired URL or a full disk - and the container log
|
|
# does not mention the 403 either.
|
|
# 2. That protection is an accident, not error handling. It rests entirely on
|
|
# mkvmerge rejecting an HTML page. Any geo response mkvmerge would accept
|
|
# is reported as Completed, and then a broken file walks into the library.
|
|
# It did not happen in either measurement round (Akamai consistently
|
|
# returns HTML), but that is not something to rely on.
|
|
#
|
|
# ----------------------------------------------------------------------------
|
|
# OPEN QUESTION, EXPLICITLY UNMEASURED: HLS
|
|
# ----------------------------------------------------------------------------
|
|
# Part of the catalogue is served as an HLS playlist (application/x-mpegURL)
|
|
# instead of a progressive MP4. The image carries an ffmpeg-based M3U8 path
|
|
# (log lines "Starting M3U8 download" / "M3U8 download completed", and ffmpeg is
|
|
# present in the image), so the code is there - but whether such an entry
|
|
# actually completes end to end has NOT been measured. If it does not, those
|
|
# entries fail regardless of any geo block. Stated as an open question on
|
|
# purpose: no assumption in either direction.
|
|
mediathekarr:
|
|
enabled: false
|
|
# Pinned BY DIGEST, unlike every other image in this chart, because the tags
|
|
# are unusable: "latest" is from 2024-11-12 (= v0.2.5) and the upstream README
|
|
# points at "beta", which is a moving tag. tag + digest together keep the
|
|
# human-readable line and the immutable reference.
|
|
#
|
|
# The image is SINGLE-ARCH, linux/amd64 only - there is no arm64 variant. The
|
|
# target cluster is amd64 throughout, so this is a note, not a problem; on a
|
|
# mixed cluster the pod would need a nodeSelector this chart does not offer.
|
|
#
|
|
# Renovate: the built-in helm-values manager cannot follow this pair (the tag
|
|
# carries no version), so renovate.json5 has a customManagers block that
|
|
# matches exactly these three lines and proposes digest updates for the beta
|
|
# tag. Renaming or reordering the keys silently switches that off - the rule
|
|
# would still be valid, it just would not match any more (#158).
|
|
image:
|
|
repository: docker.io/pcjones/mediathekarr
|
|
tag: beta
|
|
digest: sha256:3ed7e1487810e1519301bd56a0c420e260e428295b029a47806c0397e4a52a33
|
|
# TWO ports, not one, and the upstream documentation only mentions the first:
|
|
# 5007 MediathekArrDownloader - the SABnzbd role and the web UI
|
|
# 5008 MediathekArrServer - the Newznab role Prowlarr talks to
|
|
# The SABnzbd API lives under /download/api, NOT under /api: a request to
|
|
# /api on 5007 answers with a Newznab validation error and sends you looking
|
|
# in the wrong place. Both ports are declared to gluetun's firewall
|
|
# automatically (see the firewallInputPorts helper).
|
|
#
|
|
# No ingress by default: MediathekArr's UI links its assets with root-absolute
|
|
# paths (/download/css/..., measured), so it can only be served at the ROOT of
|
|
# a host - a sub-path is broken even behind a prefix-stripping rewrite,
|
|
# because the browser then requests the assets at the host root. On the shared
|
|
# domain the root belongs to seerr, so enabling this ingress there needs
|
|
# either seerr switched off or a host of its own here. Both consumers reach
|
|
# MediathekArr over localhost and need no ingress at all; the UI is only
|
|
# needed to run the setup wizard, for which a port-forward is enough.
|
|
ingress:
|
|
enabled: false
|
|
host: null
|
|
# Read only in the shared-domain mode. "/" is the only value that works;
|
|
# anything else fails the render rather than producing a broken UI.
|
|
path: /
|
|
# clusterIssuer: null
|
|
# className: null
|
|
# annotations: {}
|
|
storage:
|
|
# Size of the MediathekArr config volume (/app/config, a small JSON).
|
|
size: 1Gi
|
|
# THE START GATE - the sharpest point of this container, and the one whose
|
|
# failure mode looks green.
|
|
#
|
|
# If MediathekArr starts before the tunnel is up, its ruleset fetch fails once
|
|
# and is never retried: the instance then answered every Newznab search with 0
|
|
# results for over eleven minutes, while the container was up, Ready and
|
|
# logged nothing wrong. A green pod proves nothing here. With the start
|
|
# delayed until gluetun reports healthy, the ruleset loads and the search
|
|
# matches an instance running with direct egress (the search itself is not
|
|
# geo-blocked - 56 hits through either exit).
|
|
#
|
|
# Kubernetes cannot express "start after that container": gluetun is a regular
|
|
# container, and an initContainer waiting for it would deadlock. So the gate
|
|
# lives in this container's command - it polls gluetun's health server on
|
|
# 127.0.0.1:9999 (the same endpoint gluetun's own probes use, 200 only while
|
|
# the tunnel works) and only then execs the image entrypoint.
|
|
#
|
|
# On timeout the container EXITS instead of starting anyway. That is
|
|
# deliberate: a crash-looping container is visible, a silently empty indexer
|
|
# is not.
|
|
#
|
|
# Keep startupProbe budget > timeoutSeconds + app start, or the kubelet kills
|
|
# the container while the gate is still waiting. Defaults: 180s gate,
|
|
# 5s * 60 = 300s startup budget.
|
|
startGate:
|
|
enabled: true
|
|
# Poll interval and total budget for gluetun becoming healthy. gluetun's own
|
|
# startup budget is 150s, so 180s covers a normal handshake with room.
|
|
intervalSeconds: 3
|
|
timeoutSeconds: 180
|
|
# Extra pause after gluetun reports healthy, before the app starts. NOT a
|
|
# measured requirement - gluetun's 200 is the actual condition - but a cheap
|
|
# buffer for DNS and routing to settle. 0 disables it.
|
|
settleSeconds: 10
|
|
# WHERE THE DOWNLOADS GO. Both directories live on the SHARED downloads claim
|
|
# (media.downloads), mounted at /downloads exactly like sonarr, radarr and
|
|
# qbittorrent mount it, under this sub-directory:
|
|
#
|
|
# /downloads/<directory>/incomplete work in progress
|
|
# /downloads/<directory>/complete finished, plus one sub-dir per category
|
|
#
|
|
# Two reasons, both of them the kind that fails silently:
|
|
#
|
|
# 1. complete/ MUST be visible to Sonarr/Radarr under the SAME absolute
|
|
# path. The chart hands that path to MediathekArr
|
|
# (DOWNLOAD_COMPLETE_PATH), which reports it to Sonarr as the SABnzbd
|
|
# complete_dir. Every container here mounts the claim at /downloads, so
|
|
# the paths are identical by construction - a deviation imports nothing,
|
|
# without an error.
|
|
# 2. incomplete/ must NOT be an emptyDir. An emptyDir counts against the
|
|
# container's ephemeral-storage limit, and one large download makes the
|
|
# kubelet evict the WHOLE pod - all ten containers. Same trap as the one
|
|
# documented in ytdlp-web-player.
|
|
#
|
|
# Do not set DOWNLOAD_COMPLETE_PATH or DOWNLOAD_INCOMPLETE_PATH in the env
|
|
# block below: the chart always sets both, and a second entry of the same name
|
|
# is not a defined state (see README, "Environment variables").
|
|
downloads:
|
|
directory: mediathekarr
|
|
# Extra environment variables, appended to the ones the chart always sets.
|
|
# Useful ones the image reads: CATEGORIES, MAX_PARALLEL_DOWNLOADS,
|
|
# MEDIATHEKARR_API_BASE_URL.
|
|
env: []
|
|
# Probes on measured 200 endpoints - MediathekArr has NO health endpoint at
|
|
# all (/health, /healthz, /api/health, /download/health, /status and /ping all
|
|
# answer 404).
|
|
#
|
|
# Liveness is an exec on purpose: one container runs TWO processes, and the
|
|
# kubelet allows one probe of each kind. The Newznab half can wedge while the
|
|
# downloader half still answers, and that is exactly the failure that makes
|
|
# searches come back empty. The command checks both ports and restarts the
|
|
# container when either stops answering. (The entrypoint already exits when
|
|
# one of the two processes CRASHES - the exec probe covers the case where it
|
|
# is alive but not serving.)
|
|
#
|
|
# Readiness and startup use the downloader port: that is the one the Service
|
|
# and any ingress point at, and /download/ answers 200 without a query string.
|
|
livenessProbe:
|
|
exec:
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- >-
|
|
wget -q -T 5 -O /dev/null "http://127.0.0.1:5008/api?t=caps" &&
|
|
wget -q -T 5 -O /dev/null "http://127.0.0.1:5007/download/api?mode=version&output=json&apikey=x"
|
|
periodSeconds: 30
|
|
timeoutSeconds: 15
|
|
failureThreshold: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /download/
|
|
port: 5007
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
# The startup probe gates liveness/readiness AND has to outlast the start gate
|
|
# (see startGate above): 5s * 60 = 300s against a 180s gate.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /download/
|
|
port: 5007
|
|
periodSeconds: 5
|
|
timeoutSeconds: 5
|
|
failureThreshold: 60
|
|
resources:
|
|
limitFactor: 3
|
|
# ephemeral-storage stays small on purpose: downloads land on the media
|
|
# claim, not on the container filesystem (see downloads above).
|
|
requests:
|
|
cpu: 0.1
|
|
memory: 256Mi
|
|
ephemeralStorage: 100Mi
|
|
# Measured, including a real download and the mkvmerge remux: the container
|
|
# runs fully hardened - non-root, uid/gid from puid/pgid,
|
|
# readOnlyRootFilesystem: true - as long as its writable paths are volumes.
|
|
# Those are /app/config (the config claim) and the two download directories on
|
|
# the media claim; the image's built-in default /data/mediathek is never
|
|
# touched, because the chart overrides both paths by environment (measured:
|
|
# read-only root, no /data/mediathek mount, no error).
|
|
# PUID/PGID are NOT needed (the image's gosu path is only taken when they are
|
|
# set), and the
|
|
# commented-out non-root block in its Dockerfile is outdated for the beta.
|
|
# runAsUser/runAsGroup come from the global puid/pgid values in the template -
|
|
# do not set them here.
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|