freesailarr: shared domain with sub-paths, qBittorrent save path (3.0.0+up1.0.0)
Adds a second, optional ingress shape and fixes the download target of qBittorrent. The existing per-app-host shape stays the default and renders unchanged. Shared domain (#167) Setting ingress.host moves every app without a host of its own into one Ingress object with one path each (<app>.ingress.path, default /<app>), one TLS secret and therefore one DNS name and one certificate for the whole stack. A per-app host always wins and keeps its own object, so mixed operation needs no extra values. The apps differ in what they can do under a sub-path, and the chart follows that rather than pretending they are alike: - radarr, sonarr, prowlarr get <APP>__SERVER__URLBASE from the chart, but only when their own env block does not already carry the variable. - bazarr has no environment variable for its base path and stays one documented manual step (Settings -> General -> Base URL). - qbittorrent has no base path at all and renders a second object on the same host with a prefix-stripping rewrite. It repeats the same TLS secret name and deliberately omits the cert-manager annotation, so both objects state their own TLS while only one certificate is requested. - seerr has no base path either and cannot be rewritten, so it is pinned to "/" and a fail refuses any other path. A second fail refuses two apps claiming the same path. qBittorrent save path (#166) Without Session\DefaultSavePath qBittorrent writes to its built-in default /config/Downloads - the 1Gi config volume. It fills within minutes, every torrent goes to "errored" and qBittorrent can no longer write its own metadata to BT_backup, so a restart in that state costs the torrent list. The init container (renamed qbittorrent-config-interface -> qbittorrent-config-seed, since it no longer only sets the interface) now seeds qbittorrent.savePath, default /downloads. Unlike the interface binding it is inserted ONLY when the key is absent and never replaced: a path changed in the WebUI is a legitimate operator decision and must survive a restart. This is the reason for the major bump: an installation that never set the path writes somewhere else after the upgrade. Documentation NOTES.txt and the media block in values.yaml now spell out why a shared claim with three subPaths does not produce hardlinks: the kernel compares the vfsmount in do_linkat, not the superblock, so two bind mounts of one filesystem are EXDEV - while stat reports the same device number on both sides and suggests the opposite. The layout that would work (one claim, one mount, categories as sub-directories) is named as a possibility; the chart cannot express it today. README.md gains the shared domain as the second admissible ingress shape of the style guideline.
This commit is contained in:
8 files changed
+558
-45
No files matched your search
@@ -9,7 +9,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
| `apache-tika` | Apache Tika | Deployment + Service only (no ingress) |
|
||||
| `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC |
|
||||
| `cyberchef` | CyberChef | Stateless Deployment |
|
||||
| `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr) | Single-pod StatefulSet: all apps share gluetun's network namespace; config volumes via volumeClaimTemplates, media PVCs as existingClaims (optional per-category `subPath`, so one claim can serve all three categories). One ClusterIP Service and one Ingress **per app** — no global `ingress.domain`, each app has its own required host (documented ingress exception below) |
|
||||
| `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr) | Single-pod StatefulSet: all apps share gluetun's network namespace; config volumes via volumeClaimTemplates, media PVCs as existingClaims (optional per-category `subPath`, so one claim can serve all three categories). One ClusterIP Service per app; no global `ingress.domain`. Ingress in two shapes that mix: one host per app (default, each app's host required), or one shared `ingress.host` with the apps as sub-paths (documented ingress exception below) |
|
||||
| `gotenberg` | Gotenberg | Deployment + Service only (no ingress) |
|
||||
| `homeassistant` | Home Assistant | StatefulSet, has `NOTES.txt` and Helm tests |
|
||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||
@@ -158,7 +158,8 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
- `ingress.annotations` takes optional extra annotations that are merged with the cert-manager `cluster-issuer` annotation the chart always sets.
|
||||
- The TLS secret is named `tls-<release>-<chart>-ingress`.
|
||||
- **Documented exception — homeassistant**: keeps its upstream-style flexible ingress values (host/tls lists, no required values); only its `ingress.className` default follows the standard (`nginx`, clearable to omit the field).
|
||||
- **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. Each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app.
|
||||
- **A shared domain with sub-paths is the second admissible ingress shape.** Where a chart exposes several apps, they may either each take a host of their own or share one host and be told apart by path (`domain.tld/sonarr`, `domain.tld/radarr`, …) — one DNS name and one certificate for the whole bundle. The rules for the shared shape: the domain is a single value (`ingress.host`), each app's path is its own value defaulting to `/<app>`, the paths render in one Ingress object `<release>-<chart>-ingress` with the standard TLS secret `tls-<release>-<chart>-ingress`, and an app that carries a host of its own always wins and keeps its own object — so both shapes mix without extra values. An app that needs a per-path proxy annotation (`rewrite-target` is per **object**, not per path) gets its own object on the same host, repeating the same TLS secret name and **omitting** the cert-manager annotation, so that exactly one certificate is requested. Apps that cannot serve a base path must be rendered at `/` or refused with a `fail` — never rendered somewhere they provably cannot work.
|
||||
- **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. It offers both shapes. By default each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled and it is not on the shared domain) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. Setting the global `ingress.host` switches every app without a host of its own to the shared-domain shape under `<app>.ingress.path`; qBittorrent (no base path, needs a prefix-stripping rewrite) gets the extra object described above, and seerr is pinned to `/`. `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app.
|
||||
|
||||
### 5. Security hardening
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack
|
||||
|
||||
type: application
|
||||
|
||||
version: 2.0.2+up1.0.0
|
||||
version: 3.0.0+up1.0.0
|
||||
# The chart's own version marker, freely chosen and tied to no upstream. The
|
||||
# stack bundles nine independently versioned projects, so there is no single
|
||||
# "app version" to track here.
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
{{- $budget := mul (int $startup.periodSeconds) (int $startup.failureThreshold) -}}
|
||||
{{- $ingressApps := include "freesailarr.ingressApps" . | fromJsonArray -}}
|
||||
{{- $portApps := include "freesailarr.portApps" . | fromJsonArray -}}
|
||||
{{- $sharedHost := .Values.ingress.host | default "" -}}
|
||||
{{- $shared := include "freesailarr.sharedIngressApps" . | fromJsonArray -}}
|
||||
{{- $sharedNames := list -}}
|
||||
{{- range $shared }}{{ $sharedNames = append $sharedNames .name }}{{ end -}}
|
||||
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
Nine containers in ONE pod, sharing gluetun's network namespace.
|
||||
|
||||
@@ -41,13 +45,53 @@ SET gluetun.vpn.firewallOutboundSubnets — IT IS EMPTY RIGHT NOW
|
||||
|
||||
ACCESS
|
||||
{{- if and .Values.ingress.enabled $ingressApps }}
|
||||
{{- range $shared }}
|
||||
{{ .name }}: https://{{ $sharedHost }}{{ if ne .path "/" }}{{ .path }}{{ end }}
|
||||
{{- end }}
|
||||
{{- range $ingressApps }}
|
||||
{{- $cfg := (index $.Values .name).ingress }}
|
||||
{{- if not (has .name $sharedNames) }}
|
||||
{{ .name }}: https://{{ $cfg.host }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $shared }}
|
||||
|
||||
Each app has its OWN Ingress and its own TLS secret; while cert-manager is
|
||||
still issuing, the browser warns:
|
||||
These apps share one host, one Ingress and one certificate.
|
||||
{{- if has "bazarr" $sharedNames }}
|
||||
|
||||
BAZARR NEEDS ONE MANUAL STEP UNDER A SUB-PATH
|
||||
Bazarr reads its base path only from its own configuration - it has no
|
||||
environment variable for it, so the chart cannot set it. Open bazarr,
|
||||
go to Settings -> General -> Base URL, enter
|
||||
|
||||
{{ include "freesailarr.appPath" (dict "root" $ "app" "bazarr") }}
|
||||
|
||||
and restart the pod. Until then bazarr answers only at the root and the
|
||||
sub-path shows a broken UI. radarr, sonarr and prowlarr need nothing -
|
||||
the chart sets their UrlBase itself.
|
||||
{{- end }}
|
||||
{{- if has "qbittorrent" $sharedNames }}
|
||||
|
||||
QBITTORRENT SITS BEHIND A PREFIX-STRIPPING REWRITE
|
||||
It has no base path of its own, so it gets a second Ingress object on the
|
||||
same host whose rewrite cuts the prefix off. Two things there look exactly
|
||||
like a login failure and send you looking in the wrong place: its session
|
||||
cookie is marked "Secure" (over plain HTTP the browser never sends it back
|
||||
and every click bounces to the login form), and it bans the client IP for a
|
||||
while after a few failed attempts. If it does not work at all, set
|
||||
qbittorrent.ingress.host and it is back on a host of its own - no chart
|
||||
change needed.
|
||||
{{- end }}
|
||||
{{- if not (has "seerr" $sharedNames) }}
|
||||
|
||||
NOTHING SERVES THE ROOT OF {{ $sharedHost }}
|
||||
seerr is the app that sits at "/" and it is not on the shared host, so
|
||||
https://{{ $sharedHost }}/ answers 404. The sub-paths are unaffected.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
Every app listed with a host of its own has its OWN Ingress and its own TLS
|
||||
secret. While cert-manager is still issuing, the browser warns:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get certificate
|
||||
{{- else }}
|
||||
@@ -62,6 +106,33 @@ ACCESS
|
||||
qBittorrent's WebUI rejects a Host header whose port differs from its own
|
||||
({{ "8080" }}) with a plain 401. A port-forward on a different local port therefore
|
||||
looks like a login failure — forward 8080:8080, or go through the ingress.
|
||||
{{- if .Values.qbittorrent.savePath }}
|
||||
|
||||
QBITTORRENT DOWNLOADS GO TO {{ .Values.qbittorrent.savePath }}
|
||||
The chart seeds Session\DefaultSavePath into qBittorrent's config, because
|
||||
its built-in default is /config/Downloads — the config volume{{ with .Values.qbittorrent.storage.size }} ({{ . }}){{ end }}, which
|
||||
fills within minutes and puts EVERY torrent into "errored" while a large,
|
||||
empty media volume sits next to it. The failure looks like a network or VPN
|
||||
problem; only "df" shows what it is.
|
||||
|
||||
The value is seeded ONLY when the key is absent, never replaced. So if you
|
||||
changed the path in the WebUI, it stays yours and this value does nothing —
|
||||
check which one is actually in effect:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} exec {{ $name }}-sfs-0 -c qbittorrent -- \
|
||||
grep SavePath /config/qBittorrent/qBittorrent.conf
|
||||
|
||||
If that shows a path under /config, downloads are landing on the config
|
||||
volume: fix it under Options -> Downloads -> Default Save Path (the running
|
||||
app owns the file; editing it under the app is overwritten on exit).
|
||||
{{- else }}
|
||||
|
||||
QBITTORRENT HAS NO SAVE PATH SET (qbittorrent.savePath is empty)
|
||||
It therefore falls back to /config/Downloads — the small config volume. That
|
||||
fills within minutes and puts every torrent into "errored". Either set
|
||||
qbittorrent.savePath or set Options -> Downloads -> Default Save Path in the
|
||||
WebUI before starting any download.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
EVERY SERVICE POINTS AT THE SAME POD IP
|
||||
@@ -87,6 +158,29 @@ THE MEDIA CLAIMS ARE YOURS, THE CONFIG VOLUMES ARE THE CHART'S
|
||||
Kubernetes makes every volumeMount its own bind mount and link() across two
|
||||
mount points fails with EXDEV regardless of subPath, so an import copies.
|
||||
|
||||
HARDLINKS NEED ONE MOUNT, NOT ONE CLAIM
|
||||
This is the point where the obvious conclusion is wrong, so it is spelled
|
||||
out: putting downloads on the same claim as movies and tv, as a third
|
||||
subPath, does NOT produce hardlinks. It buys the layout, nothing else.
|
||||
|
||||
The kernel compares the VFSMOUNT in do_linkat, not the superblock, and two
|
||||
bind mounts of the same filesystem are two vfsmounts — so link() across
|
||||
them is EXDEV even though it is one volume. Measured on Longhorn/NFS: both
|
||||
mount points report st_dev 0:56, the mount table shows two mount IDs on
|
||||
that same device, a cross-mount link() fails, a link() inside one mount
|
||||
succeeds with link count 2.
|
||||
|
||||
Note the trap in that measurement: "stat" reports the SAME device number
|
||||
on both sides. Every intuition says a hardlink must work there. It does
|
||||
not, and the device number is exactly what makes this look like a bug in
|
||||
the app 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 "single /data mount" the *arr
|
||||
guides recommend. This chart cannot express that today: it mounts each
|
||||
category separately. Naming the possibility, not giving instructions —
|
||||
there are no values for it.
|
||||
|
||||
The containers write as {{ .Values.puid }}:{{ .Values.pgid }}. The claim's root directory has to be
|
||||
writable for them, and a subPath directory the kubelet creates inherits that
|
||||
root's mode — so fix the ownership before the first start. The pod will not
|
||||
|
||||
@@ -194,6 +194,131 @@ ingress block with enabled: true. Used by the ingress template.
|
||||
{{- toJson $apps -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The sub-path an app is served under in the shared-domain mode, e.g. "/sonarr".
|
||||
Null-safe by construction: an absent or erased <app>.ingress.path falls back to
|
||||
"/<app>", and to "/" for seerr, which has no base-path support at all and can
|
||||
therefore only live at the root.
|
||||
|
||||
Input: dict "root" $ "app" <app name>.
|
||||
*/}}
|
||||
{{- define "freesailarr.appPath" -}}
|
||||
{{- $config := (index .root.Values .app).ingress -}}
|
||||
{{- $path := "" -}}
|
||||
{{- if kindIs "map" $config -}}
|
||||
{{- $path = $config.path -}}
|
||||
{{- end -}}
|
||||
{{- if or (kindIs "invalid" $path) (eq (toString $path) "") -}}
|
||||
{{- if eq .app "seerr" -}}
|
||||
{{- $path = "/" -}}
|
||||
{{- else -}}
|
||||
{{- $path = printf "/%s" .app -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $path -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The apps served under the shared domain (ingress.host), as a JSON array of
|
||||
{name, port, path} dicts, sorted by path length descending so the rendered list
|
||||
is deterministic.
|
||||
|
||||
The shared-domain mode is off unless ingress.host is set; then it covers the
|
||||
apps whose ingress renders at all (freesailarr.ingressApps, i.e. the enabled
|
||||
port apps with ingress.enabled) and which do not carry a host of their own. A
|
||||
per-app host always wins and keeps rendering its own object exactly as before,
|
||||
which is what makes mixed operation fall out for free.
|
||||
|
||||
Two guards live here rather than in the ingress template, so that every
|
||||
consumer - the ingress objects, the UrlBase env vars and NOTES.txt - sees the
|
||||
same validated list:
|
||||
|
||||
1. seerr under any path other than "/". seerr has no base-path support
|
||||
(upstream request open since May 2022), so a chart that rendered it under
|
||||
a sub-path would produce something that provably cannot work.
|
||||
2. Two apps claiming the same path on the shared host. Only paths on the
|
||||
shared host can collide; apps with their own host are on a different host
|
||||
and are therefore not part of this check.
|
||||
|
||||
{{- $shared := include "freesailarr.sharedIngressApps" . | fromJsonArray }}
|
||||
*/}}
|
||||
{{- define "freesailarr.sharedIngressApps" -}}
|
||||
{{- $root := . -}}
|
||||
{{- $apps := list -}}
|
||||
{{- $host := $root.Values.ingress.host | default "" -}}
|
||||
{{- if and $root.Values.ingress.enabled $host -}}
|
||||
{{- $seen := dict -}}
|
||||
{{- range $app := include "freesailarr.ingressApps" $root | fromJsonArray -}}
|
||||
{{- $config := (index $root.Values $app.name).ingress -}}
|
||||
{{- if not ($config.host | default "") -}}
|
||||
{{- $path := include "freesailarr.appPath" (dict "root" $root "app" $app.name) -}}
|
||||
{{- if and (eq $app.name "seerr") (ne $path "/") -}}
|
||||
{{- fail (printf "seerr.ingress.path is %q, but seerr has no base-path support and can only be served at \"/\" (https://github.com/Fallenbagel/jellyseerr/issues/97). Either leave seerr.ingress.path at \"/\" or give seerr its own seerr.ingress.host." $path) -}}
|
||||
{{- end -}}
|
||||
{{- if hasKey $seen $path -}}
|
||||
{{- fail (printf "%s.ingress.path and %s.ingress.path are both %q on the shared host %q - every app under ingress.host needs its own path" (index $seen $path) $app.name $path $host) -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $seen $path $app.name -}}
|
||||
{{- $apps = append $apps (dict "name" $app.name "port" $app.port "path" $path) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- /* Sort by path length descending, ties broken alphabetically. The keys are
|
||||
unique because the guard above rejects duplicate paths. */ -}}
|
||||
{{- $keyed := dict -}}
|
||||
{{- $keys := list -}}
|
||||
{{- range $app := $apps -}}
|
||||
{{- $key := printf "%03d|%s" (sub 999 (len $app.path)) $app.path -}}
|
||||
{{- $_ := set $keyed $key $app -}}
|
||||
{{- $keys = append $keys $key -}}
|
||||
{{- end -}}
|
||||
{{- $sorted := list -}}
|
||||
{{- range $key := sortAlpha $keys -}}
|
||||
{{- $sorted = append $sorted (index $keyed $key) -}}
|
||||
{{- end -}}
|
||||
{{- toJson $sorted -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The value for a servarr app's <APP>__SERVER__URLBASE variable, or the empty
|
||||
string when the chart must not set it. Radarr, Sonarr and Prowlarr read their
|
||||
base path from this environment variable (schema
|
||||
<APP>__CONFIGNAMESPACE__CONFIGITEM, https://wiki.servarr.com/sonarr/environment-variables),
|
||||
so the chart can configure them itself as soon as they render under a sub-path.
|
||||
|
||||
Empty - i.e. the chart sets nothing - in three cases:
|
||||
|
||||
- the app does not render under the shared host, or renders at "/";
|
||||
- the app's own env block already carries the variable. The env blocks are
|
||||
documented as additive; emitting a second entry with the same name would
|
||||
break that promise, and two identically named variables in one container
|
||||
are not a defined state to build on.
|
||||
|
||||
Input: dict "root" $ "app" <"radarr"|"sonarr"|"prowlarr">.
|
||||
*/}}
|
||||
{{- define "freesailarr.urlBase" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $app := .app -}}
|
||||
{{- $var := printf "%s__SERVER__URLBASE" (upper $app) -}}
|
||||
{{- $path := "" -}}
|
||||
{{- range $entry := include "freesailarr.sharedIngressApps" $root | fromJsonArray -}}
|
||||
{{- if eq $entry.name $app -}}
|
||||
{{- $path = $entry.path -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if eq $path "/" -}}
|
||||
{{- $path = "" -}}
|
||||
{{- end -}}
|
||||
{{- if $path -}}
|
||||
{{- range $entry := (index $root.Values $app).env -}}
|
||||
{{- if eq (toString $entry.name) $var -}}
|
||||
{{- $path = "" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $path -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Comma-separated FIREWALL_INPUT_PORTS for gluetun: the ports of the enabled app
|
||||
containers (ingress traffic and the kubelet probes both arrive on the pod IP)
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
{{/*
|
||||
One Ingress object per app whose ingress is enabled, each with its own host and
|
||||
its own TLS secret. The hosts are independent values - they do not have to
|
||||
share a domain - and each ingress points at its own app's service.
|
||||
Two ingress shapes, and they mix freely.
|
||||
|
||||
<app>.ingress.host is required, but only for an app whose ingress is actually
|
||||
enabled: the required call sits inside the loop over the enabled apps, so
|
||||
turning an app's ingress off must never make the chart demand a host for it.
|
||||
1. One host per app (the default): every app whose ingress is enabled gets its
|
||||
own Ingress object with its own host and its own TLS secret. The hosts are
|
||||
independent values - they do not have to share a domain - and each ingress
|
||||
points at its own app's service.
|
||||
|
||||
<app>.ingress.host is required, but only for an app whose ingress is
|
||||
actually enabled and which is not served under the shared domain: the
|
||||
required call sits inside the loop over the enabled apps, so turning an
|
||||
app's ingress off must never make the chart demand a host for it.
|
||||
|
||||
2. One shared domain with sub-paths (ingress.host): the apps that carry no host
|
||||
of their own move into ONE object with one path each, one TLS secret, and
|
||||
therefore one DNS name and one certificate for the whole stack. An app that
|
||||
does keep its own host stays in shape 1.
|
||||
|
||||
qBittorrent renders a second object on the same host in shape 2 - see the
|
||||
comment on that block.
|
||||
*/}}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $sharedHost := .Values.ingress.host | default "" }}
|
||||
{{- $shared := include "freesailarr.sharedIngressApps" $ | fromJsonArray }}
|
||||
{{- /* qBittorrent needs a rewrite annotation the others must not get, so it is
|
||||
split out of the shared object and rendered separately below. */}}
|
||||
{{- $sharedMain := list }}
|
||||
{{- $sharedQbt := dict }}
|
||||
{{- range $app := $shared }}
|
||||
{{- if eq $app.name "qbittorrent" }}
|
||||
{{- $sharedQbt = $app }}
|
||||
{{- else }}
|
||||
{{- $sharedMain = append $sharedMain $app }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $app := include "freesailarr.ingressApps" $ | fromJsonArray }}
|
||||
{{- $config := (index $.Values $app.name).ingress }}
|
||||
{{- /* clusterIssuer, className and annotations default to the global ingress
|
||||
@@ -19,7 +44,9 @@ turning an app's ingress off must never make the chart demand a host for it.
|
||||
{{- if not (kindIs "invalid" $config.className) }}{{- $className = $config.className }}{{- end }}
|
||||
{{- $annotations := $.Values.ingress.annotations }}
|
||||
{{- if not (kindIs "invalid" $config.annotations) }}{{- $annotations = $config.annotations }}{{- end }}
|
||||
{{- $host := required (printf "%s.ingress.host must be provided while %s.ingress.enabled is true" $app.name $app.name) $config.host }}
|
||||
{{- /* Skip the apps that live on the shared domain; they are rendered below. */}}
|
||||
{{- if or (not $sharedHost) ($config.host | default "") }}
|
||||
{{- $host := required (printf "%s.ingress.host must be provided while %s.ingress.enabled is true (or set the shared ingress.host to serve the app under a sub-path instead)" $app.name $app.name) $config.host }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
@@ -49,3 +76,95 @@ spec:
|
||||
secretName: tls-{{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-ingress
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $sharedMain }}
|
||||
{{/*
|
||||
The shared-domain object. Rendered only when at least one non-qBittorrent app
|
||||
actually contributes a path, so switching every app ingress off while leaving
|
||||
ingress.host set produces NO object rather than an empty one without rules.
|
||||
|
||||
The paths come sorted by length descending. nginx matches the longest prefix
|
||||
regardless of order, so this is about a deterministic, reviewable rendering,
|
||||
not about matching behaviour.
|
||||
*/}}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ $sharedHost | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range $app := $sharedMain }}
|
||||
- backend:
|
||||
service:
|
||||
name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-service
|
||||
port:
|
||||
name: {{ $app.name }}
|
||||
path: {{ $app.path | quote }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $sharedHost | quote }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
{{- if $sharedQbt }}
|
||||
{{/*
|
||||
qBittorrent on the shared domain, as its own object on the same host.
|
||||
|
||||
qBittorrent has no base-path setting: it serves its WebUI at the root and the
|
||||
proxy has to strip the prefix
|
||||
(https://github.com/qbittorrent/wiki/blob/master/NGINX-Reverse-Proxy-for-Web-UI.md).
|
||||
nginx applies rewrite-target per INGRESS OBJECT, not per path, and the other
|
||||
apps must not be rewritten - their UrlBase contains the prefix, so they expect
|
||||
the full path. Hence the split.
|
||||
|
||||
The tls block repeats the SAME secret name as the main object on purpose: this
|
||||
way neither object depends on an assumption about whether nginx applies the
|
||||
other one's TLS configuration to its paths - both state it themselves. The
|
||||
cert-manager annotation is deliberately absent, so only one Certificate is
|
||||
requested for that one secret. The one case where it IS set here is when no
|
||||
main object renders at all (qBittorrent is the only app on the shared host) -
|
||||
otherwise nothing would ever request the certificate.
|
||||
*/}}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-qbittorrent-ingress
|
||||
annotations:
|
||||
# Strip the path prefix: capture group 2 is everything after "<path>/".
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||
{{- if not $sharedMain }}
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ $sharedHost | quote }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-qbittorrent-service
|
||||
port:
|
||||
name: qbittorrent
|
||||
path: {{ printf "%s(/|$)(.*)" $sharedQbt.path | quote }}
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $sharedHost | quote }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -34,21 +34,31 @@ spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "freesailarr.podSecurityContext" . | nindent 8 }}
|
||||
{{- if and .Values.qbittorrent.enabled .Values.qbittorrent.networkInterface }}
|
||||
{{- if and .Values.qbittorrent.enabled (or .Values.qbittorrent.networkInterface .Values.qbittorrent.savePath) }}
|
||||
initContainers:
|
||||
# qBittorrent has no environment variable for the session's network
|
||||
# interface - it only reads it from the persisted config file, which
|
||||
# the app itself creates on first start. Without the binding
|
||||
# qBittorrent reads neither the session's network interface nor its
|
||||
# default save path from the environment - both live only in the
|
||||
# persisted config file, which the app itself creates on first start.
|
||||
# This container seeds them before qbittorrent starts, so a fresh
|
||||
# install works out of the box. Both edits preserve all other settings:
|
||||
# keys are inserted into the [BitTorrent] section (qBittorrent 5.x
|
||||
# reads them there), and a missing file or section is created.
|
||||
#
|
||||
# The two keys are handled DIFFERENTLY on purpose:
|
||||
#
|
||||
# - Session\Interface is ENFORCED on every start. Without the binding
|
||||
# libtorrent listens and announces on every interface of the shared
|
||||
# pod network namespace (eth0/pod IP and loopback included); gluetun's
|
||||
# kill switch drops those packets, trackers stay unreachable and
|
||||
# torrents stall at 0 bytes. Seeding the two keys before qbittorrent
|
||||
# starts closes that leak and makes a fresh install work out of the
|
||||
# box. The edit is idempotent and preserves all other settings:
|
||||
# existing keys are replaced, missing ones are inserted into the
|
||||
# [BitTorrent] section (qBittorrent 5.x reads them there), and a
|
||||
# missing file or section is created.
|
||||
- name: qbittorrent-config-interface
|
||||
# pod network namespace (eth0/pod IP and loopback included);
|
||||
# gluetun's kill switch drops those packets, trackers stay
|
||||
# unreachable and torrents stall at 0 bytes. A deviating value is a
|
||||
# leak, so an existing key is replaced.
|
||||
# - Session\DefaultSavePath is only INSERTED WHEN ABSENT, never
|
||||
# replaced. Without it qBittorrent falls back to /config/Downloads,
|
||||
# i.e. the small config volume, which fills within minutes and puts
|
||||
# every torrent into "errored" (#166). But where downloads go is a
|
||||
# legitimate operator decision: someone who changes the path in the
|
||||
# WebUI must not have it overwritten on every pod restart.
|
||||
- name: qbittorrent-config-seed
|
||||
image: "{{ .Values.qbittorrent.configInit.image.repository }}:{{ .Values.qbittorrent.configInit.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
@@ -65,6 +75,7 @@ spec:
|
||||
printf '\n' >> "$conf"
|
||||
fi
|
||||
grep -q '^\[BitTorrent\]$' "$conf" || printf '[BitTorrent]\n' >> "$conf"
|
||||
{{- if .Values.qbittorrent.networkInterface }}
|
||||
# Drop any existing binding keys, then re-insert them directly
|
||||
# after the [BitTorrent] header.
|
||||
awk -v ifc="$QBT_INTERFACE" '
|
||||
@@ -78,9 +89,30 @@ spec:
|
||||
' "$conf" > "${conf}.tmp"
|
||||
mv "${conf}.tmp" "$conf"
|
||||
echo "qBittorrent session bound to interface $QBT_INTERFACE"
|
||||
{{- end }}
|
||||
{{- if .Values.qbittorrent.savePath }}
|
||||
# Insert only when the key is absent - an existing value is the
|
||||
# operator's and stays untouched.
|
||||
if grep -q '^Session\\DefaultSavePath=' "$conf"; then
|
||||
echo "qBittorrent save path already set, left untouched: $(grep -m1 '^Session\\DefaultSavePath=' "$conf")"
|
||||
else
|
||||
awk -v sp="$QBT_SAVE_PATH" '
|
||||
{ print }
|
||||
/^\[BitTorrent\]$/ { print "Session\\DefaultSavePath=" sp }
|
||||
' "$conf" > "${conf}.tmp"
|
||||
mv "${conf}.tmp" "$conf"
|
||||
echo "qBittorrent default save path seeded: $QBT_SAVE_PATH"
|
||||
fi
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if .Values.qbittorrent.networkInterface }}
|
||||
- name: QBT_INTERFACE
|
||||
value: {{ .Values.qbittorrent.networkInterface | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.qbittorrent.savePath }}
|
||||
- name: QBT_SAVE_PATH
|
||||
value: {{ .Values.qbittorrent.savePath | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "configInit" "given" .Values.qbittorrent.configInit.resources) | nindent 12 }}
|
||||
securityContext:
|
||||
@@ -239,6 +271,14 @@ spec:
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- /* Base path for the shared-domain mode: the chart sets
|
||||
UrlBase itself as soon as prowlarr renders under a
|
||||
sub-path, unless the env block above already carries
|
||||
the variable - see the freesailarr.urlBase helper. */}}
|
||||
{{- with (include "freesailarr.urlBase" (dict "root" $ "app" "prowlarr")) }}
|
||||
- name: PROWLARR__SERVER__URLBASE
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.prowlarr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 9696
|
||||
@@ -300,6 +340,14 @@ spec:
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- /* Base path for the shared-domain mode: the chart sets
|
||||
UrlBase itself as soon as radarr renders under a
|
||||
sub-path, unless the env block above already carries
|
||||
the variable - see the freesailarr.urlBase helper. */}}
|
||||
{{- with (include "freesailarr.urlBase" (dict "root" $ "app" "radarr")) }}
|
||||
- name: RADARR__SERVER__URLBASE
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.radarr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 7878
|
||||
@@ -368,6 +416,14 @@ spec:
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- /* Base path for the shared-domain mode: the chart sets
|
||||
UrlBase itself as soon as sonarr renders under a
|
||||
sub-path, unless the env block above already carries
|
||||
the variable - see the freesailarr.urlBase helper. */}}
|
||||
{{- with (include "freesailarr.urlBase" (dict "root" $ "app" "sonarr")) }}
|
||||
- name: SONARR__SERVER__URLBASE
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.sonarr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 8989
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"description": "Defaults for the per-app ingresses. There is deliberately no global domain - each app carries its own host.",
|
||||
"description": "Ingress defaults. Two shapes: one host per app (each app carries its own host), or one shared domain with sub-paths (ingress.host set, per-app hosts empty). A per-app host always wins, so both mix.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -130,6 +130,13 @@
|
||||
"description": "Master switch; false suppresses every app ingress regardless of the per-app toggles.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"host": {
|
||||
"description": "Shared domain for the sub-path mode. Empty or null means the default: every app on a host of its own.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"clusterIssuer": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -238,7 +245,14 @@
|
||||
"$ref": "#/definitions/appStorage"
|
||||
},
|
||||
"networkInterface": {
|
||||
"description": "Interface the BitTorrent session binds to (gluetun's tun device). null or \"\" removes the init container that seeds it and leaves the binding unset - which lets libtorrent announce past the tunnel.",
|
||||
"description": "Interface the BitTorrent session binds to (gluetun's tun device). Enforced on every start, because a deviating value would be a leak. null or \"\" leaves the binding unset - which lets libtorrent announce past the tunnel.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savePath": {
|
||||
"description": "Default save path for new torrents, seeded into the persisted config ONLY when the key is absent - never replaced, so a path changed in the WebUI survives a restart. null or \"\" opts out and leaves qBittorrent on its built-in default /config/Downloads, i.e. the small config volume.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -548,7 +562,14 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"host": {
|
||||
"description": "Required while this app's ingress is enabled, and never demanded when it is off.",
|
||||
"description": "Host of this app's own Ingress object. Required while this app's ingress is enabled AND the shared ingress.host is not set; never demanded when this app's ingress is off, and never demanded for an app served under the shared domain. Setting it always wins over the shared domain.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "Sub-path under the shared ingress.host, e.g. /sonarr. Read only in the shared-domain mode and ignored while this app has a host of its own. Null or empty falls back to /<app>, and to / for seerr, which cannot be served anywhere else.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
|
||||
+114
-17
@@ -149,10 +149,23 @@ storage:
|
||||
# supported cross-category hardlinks.
|
||||
# - Consequence today: an import still copies, so download and library each
|
||||
# hold a full copy until the torrent is removed.
|
||||
# Getting hardlinks would mean giving the apps a single mount that contains all
|
||||
# categories (the "one /data mount" layout the *arr guides recommend) instead of
|
||||
# one mount per category - a separate change to the chart's mount layout, not a
|
||||
# values setting.
|
||||
#
|
||||
# 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
|
||||
@@ -164,15 +177,68 @@ media:
|
||||
existingClaim: null
|
||||
subPath: null
|
||||
|
||||
# Ingress defaults. There is deliberately no global domain: every app carries
|
||||
# its own <app>.ingress.host, so the hosts are independent of each other and
|
||||
# may live in completely different domains. 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).
|
||||
# Ingress defaults. There are two shapes, and they mix freely:
|
||||
#
|
||||
# 1. One host per app (the default). Every app carries its own
|
||||
# <app>.ingress.host, so the hosts are independent of each other and may
|
||||
# live in completely different domains. Each app gets its own Ingress
|
||||
# object and its own certificate.
|
||||
# 2. One shared domain with sub-paths: set ingress.host and leave the per-app
|
||||
# hosts empty. Every app then moves into ONE Ingress object as a sub-path
|
||||
# (<app>.ingress.path, default /<app>), which means one DNS name and one
|
||||
# certificate for the whole stack instead of six.
|
||||
#
|
||||
# A per-app host always wins: an app that has one keeps its own object even
|
||||
# while ingress.host is set, so a mixed setup needs no extra values.
|
||||
#
|
||||
# clusterIssuer, className and annotations are the defaults for all apps and
|
||||
# can be overridden per app in the <app>.ingress block (an explicitly set
|
||||
# per-app value always wins). The shared object uses the global values only -
|
||||
# it belongs to no single app.
|
||||
#
|
||||
# What the sub-path mode costs, per app, measured against the upstream
|
||||
# documentation and not assumed:
|
||||
#
|
||||
# - radarr, sonarr, prowlarr: nothing. The chart sets their UrlBase itself
|
||||
# (RADARR__SERVER__URLBASE and friends) as soon as they render under a
|
||||
# path other than "/" - unless that app's own env block already contains
|
||||
# the variable, in which case yours wins and the chart stays out.
|
||||
# - bazarr: ONE manual step. Bazarr reads its base path only from its own
|
||||
# configuration, it has no environment variable for it, so after the first
|
||||
# start set Settings -> General -> Base URL to the same value as
|
||||
# bazarr.ingress.path and restart the pod. Until then bazarr answers only
|
||||
# at the root and the sub-path yields a broken UI. Deliberately not
|
||||
# automated: seeding it would mean an init container writing into bazarr's
|
||||
# config database, which is a much bigger promise than this chart wants to
|
||||
# make. Note that bazarr's probes address "/" - if it does not answer
|
||||
# there once a base URL is set, adjust bazarr.livenessProbe/readinessProbe/
|
||||
# startupProbe accordingly (they are values, precisely for cases like this).
|
||||
# - qbittorrent: it has no base path at all, so the chart gives it its own
|
||||
# Ingress object on the same host with an nginx rewrite that strips the
|
||||
# prefix. Two traps from the upstream recipe, both of which look like a
|
||||
# login failure and therefore point in the wrong direction:
|
||||
# * qBittorrent marks its session cookie "Secure". Reaching the UI over
|
||||
# plain HTTP means the browser never sends the cookie back and every
|
||||
# click bounces to the login form. Use https.
|
||||
# * qBittorrent bans the client IP for a while after a few failed login
|
||||
# attempts. Once that has triggered, even correct credentials fail -
|
||||
# the ban has to expire (or the pod be restarted) first.
|
||||
# If it does not work behind the stripped prefix, the fallback costs no
|
||||
# chart change: set qbittorrent.ingress.host and qBittorrent is back on a
|
||||
# host of its own while everything else stays on the shared domain.
|
||||
# - seerr: it has no base-path support either and, unlike qBittorrent,
|
||||
# cannot be fixed by a rewrite (the upstream request has been open since
|
||||
# May 2022, https://github.com/Fallenbagel/jellyseerr/issues/97). It
|
||||
# therefore sits at "/" and the chart refuses to render it anywhere else.
|
||||
# If seerr is switched off, nothing serves the root and the shared domain
|
||||
# answers 404 there. That is correct and left alone on purpose.
|
||||
ingress:
|
||||
# Master switch: false suppresses every app ingress regardless of the
|
||||
# per-app toggles.
|
||||
enabled: true
|
||||
# The shared domain for the sub-path mode, e.g. "media.example.com". Empty
|
||||
# means the default: each app on a host of its own.
|
||||
host: ""
|
||||
clusterIssuer: null
|
||||
className: nginx
|
||||
# Extra annotations for the Ingress objects; merged with the cert-manager
|
||||
@@ -270,17 +336,40 @@ qbittorrent:
|
||||
# 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. clusterIssuer, className and
|
||||
# annotations may be set here to override the global ingress block.
|
||||
# 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
|
||||
@@ -290,13 +379,16 @@ qbittorrent:
|
||||
# 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-interface init
|
||||
# container below; setting it to null or "" removes that init container and
|
||||
# leaves the binding to the user (not recommended).
|
||||
# /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 into the
|
||||
# persisted qBittorrent config before the app starts. Runs as puid/pgid so
|
||||
# the config file keeps the ownership the app expects.
|
||||
# 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.
|
||||
@@ -382,6 +474,7 @@ prowlarr:
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
path: /prowlarr
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
@@ -495,6 +588,7 @@ radarr:
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
path: /radarr
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
@@ -605,6 +699,7 @@ sonarr:
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
path: /sonarr
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
@@ -662,6 +757,7 @@ seerr:
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
path: /
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
@@ -718,6 +814,7 @@ bazarr:
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
path: /bazarr
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
|
||||
Reference in new issue
Block a user