Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 7s
Validate Helm Charts / Build outline (pull_request) Successful in 7s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 8s
Validate Helm Charts / Build zipline (pull_request) Successful in 8s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 7s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 19s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 24s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 17s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 19s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 19s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 2m11s
Install-test / Install-test freesailarr (pull_request) Successful in 3m55s
Install-test / Install-test outline (pull_request) Successful in 2m44s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m52s
Install-test / Install-test zipline (pull_request) Successful in 2m6s
Validate Helm Charts / Install-test (pull_request) Successful in 14m50s
Group 2 of the #114 sweep: the charts whose behaviour hangs off an nginx annotation - calibre-web-automated, outline, paperless-ngx, zipline (proxy-body-size) and freesailarr (rewrite-target). Each gets the form the pilot (cyberchef, PR #115) fixed and group 1 (PR #116) repeated: an httpRoute block off by default, a template rendering ONLY an HTTPRoute with parentRefs - no Gateway, no gatewayClassName, no Certificate, no implementation-specific policy object - render aborts on both modes at once (naming both value paths), on an empty parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain], a schema entry closed against unknown keys with an open parentRef definition, and a NOTES.txt that names the active mode and what the mode costs. Minor, additive, defaults unchanged: `helm template` with each chart's CI values is byte-identical to origin/main for all five. The upload limit is DOCUMENTED, not rebuilt (decision 2 of the issue). The Gateway API knows no body size; a policy object would bind the chart to one implementation. So the Gateway branch of NOTES.txt says, per chart and in the chart's own terms, that the value is NOT ENFORCED and names where to set it: an e-book library (calibre), an upload service (zipline), the web-UI half of the document intake while the consume directory keeps no limit at all (paperless), and the open #12 pair (outline). Four findings that were not in the plan, each argued in the file it changes: freesailarr gated freesailarr.sharedIngressApps on ingress.enabled, and that list is read by the <APP>__SERVER__URLBASE variables as well as by the routing objects. In Gateway mode it would have come out EMPTY while the route served radarr, sonarr and prowlarr under their prefixes: UrlBase unset, assets and API calls linked at the host root, broken UI behind a Ready pod. The helper's two guards (seerr/mediathekarr only at "/", no duplicate paths) would have gone unenforced there too. Fixed with freesailarr.published (ingress.enabled OR httpRoute.enabled) - the same shape as sure.publishedOverTls in group 1, and identical to ingress.enabled while httpRoute.enabled is false. Measured: on origin/main with ingress.enabled: false the render emits ZERO URLBASE variables; with this change in Gateway mode it emits three. paperless-ngx was the last chart in the repository rendering its Ingress UNCONDITIONALLY. Without an ingress.enabled switch there is no second value path for the mutual-exclusion abort to name, and the Ingress plus its unconditional required() on clusterIssuer would have kept rendering in Gateway mode - a value demanded for nothing, which is issue #114's "Folge 1" and README guideline 12. The switch was added, defaulting to true, so the render is unchanged. paperless-ngx PAPERLESS_URL had no required() of its own: it relied on the Ingress template's, which stops firing the moment the Ingress is switchable. With httpRoute.hostnames set and ingress.domain empty it would have rendered "https://" and refused every login POST as a CSRF failure. It now carries its own required(), naming what the value is for. calibre-web-automated claimed, in values.yaml and in its schema, that ingress.domain is read nowhere but in the Ingress template. The HTTPRoute reads it, so the claim now holds only while both modes are off. Corrected rather than left standing - the same correction garage needed in group 1. freesailarr is where the Gateway API is LESS machinery than what it replaces, not a translation of it. rewrite-target applies per Ingress OBJECT, so qBittorrent (no base path, prefix must be stripped) needed a second Ingress on the shared host, which then had to repeat the first object's TLS secret name, omit the cert-manager annotation conditionally so only one Certificate is requested, and match an ImplementationSpecific regex path for the rewrite's capture group. A URLRewrite filter applies per RULE: one route, one rule per app, the filter on qBittorrent's rule, a plain PathPrefix. All four crutches gone. It is also the only chart without httpRoute.hostnames - it has no ingress.domain either, and a chart-global hostname list could not say which of the ten apps it belonged to, so the hostnames stay in ingress.host and <app>.ingress.host. CI: one render overlay per chart under ci/httproute/, so the assertion is not vacuum-green - ci/run-httproute-match-test.sh goes from 20 held assertions over 10 charts to 36 over 15. freesailarr's overlay moves bazarr onto a host of its own, so the shared-host route AND the per-app route are both rendered; without that the per-app branch would be exercised by nothing. The install-test fixtures stay in Ingress mode: the install runs one configuration per chart, cyberchef already pays that price against the real Gateway-API CRDs, and for paperless-ngx that fixture is the only install of the valkey-subchart path anywhere. README also carries a stale figure fixed here: the HTTPRoute test was still documented at the pilot's 4 assertions over 2 charts, although group 1 had taken it to 20 over 10. Not proven, by construction: that traffic flows. No Gateway controller runs in CI. Not run locally either: k3d, Docker, Trivy. The Gateway branch of NOTES.txt is EXECUTED by the render test (a template error in it fails the job, verified) but its text is printed by no pipeline stage - helm template does not emit notes, and the install-test installs in Ingress mode. Part of #114
1201 lines
49 KiB
YAML
1201 lines
49 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. (In GATEWAY MODE that second object is gone: the Gateway API's
|
|
# URLRewrite filter is per RULE, not per object, so qBittorrent is one rule
|
|
# in the shared route while its neighbours stay unrewritten. The traps
|
|
# below are the application's and apply in both modes.) 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.
|
|
#
|
|
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
|
|
# true the render aborts, naming both values. Mind that this defaults to
|
|
# TRUE, so turning the Gateway mode on means setting enabled: false in the
|
|
# same values file.
|
|
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.
|
|
#
|
|
# Read by BOTH routing modes: it names the public address of the stack and is
|
|
# not an Ingress switch. Same for the per-app <app>.ingress.host.
|
|
host: ""
|
|
clusterIssuer: null
|
|
className: nginx
|
|
# Extra annotations for the Ingress objects; merged with the cert-manager
|
|
# cluster-issuer annotation the chart always sets.
|
|
#
|
|
# NO COUNTERPART IN GATEWAY MODE, and the same goes for the per-app
|
|
# overrides: the Gateway API has no annotation channel for controller
|
|
# behaviour, so anything set here is simply gone when httpRoute.enabled is
|
|
# used instead. This chart NEEDS nothing from it - the one behaviour-bearing
|
|
# annotation it sets itself, qBittorrent's prefix-stripping rewrite, has a
|
|
# real and better equivalent in that mode (a per-rule URLRewrite filter). But
|
|
# an annotation YOU added here is lost without a word; a body-size cap for
|
|
# qBittorrent's torrent-file uploads is the likely candidate, and it then has
|
|
# to be set at the Gateway or through its implementation's own policy object.
|
|
annotations: {}
|
|
|
|
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
|
|
# an existing release is unaffected; exactly one of ingress.enabled and
|
|
# httpRoute.enabled may be true - and ingress.enabled defaults to true, so it
|
|
# has to be switched off explicitly.
|
|
#
|
|
# The chart renders ONLY HTTPRoutes with parentRefs - no Gateway, no
|
|
# gatewayClassName, no Certificate and no implementation-specific policy
|
|
# object. Class and certificate belong to the Gateway and therefore to the
|
|
# platform, which is what keeps this chart independent of whether the cluster
|
|
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
|
|
#
|
|
# Both shapes above work unchanged, and the sub-path mode gets SIMPLER: the
|
|
# Gateway API's URLRewrite filter is per rule, so qBittorrent no longer needs a
|
|
# second routing object of its own. One route, one rule per app, the filter on
|
|
# qBittorrent's rule only.
|
|
#
|
|
# There is deliberately NO httpRoute.hostnames here, unlike every other chart
|
|
# in the repository: this chart has no ingress.domain either (it bundles ten
|
|
# independently exposed apps - README, guideline 4, documented exception), so a
|
|
# chart-global list of hostnames could not say which app it belonged to. The
|
|
# hostnames come from ingress.host and <app>.ingress.host, which is where this
|
|
# chart's single source of truth for them already is.
|
|
#
|
|
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
|
|
# cannot detect them (.Capabilities.APIVersions is unreliable at template
|
|
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
|
|
httpRoute:
|
|
enabled: false
|
|
# The Gateway(s) every route of this release attaches to - a platform object,
|
|
# usually in another namespace, which must allow this namespace in its
|
|
# listener's allowedRoutes. One entry per parent, the fields are the Gateway
|
|
# API's ParentReference (name, and optionally namespace, sectionName, port,
|
|
# group, kind), passed through unchanged:
|
|
#
|
|
# parentRefs:
|
|
# - name: public
|
|
# namespace: gateway
|
|
# sectionName: https
|
|
#
|
|
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
|
|
# list, because a route without a parent is created, is valid and routes
|
|
# nothing, with no event to say so. The same list is used for every route
|
|
# this chart renders; the hosts differ, the Gateway does not.
|
|
parentRefs: []
|
|
|
|
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
|