MediathekArr fakes a Usenet indexer towards Prowlarr and a SABnzbd downloader
towards Sonarr/Radarr and fetches from the ARD and ZDF media libraries. Both
consumers already share gluetun's network namespace, so they reach it over
127.0.0.1 - no service, no DNS, no ingress detour. Purely additive: the flag
defaults to false and a default render is byte-identical to 3.0.5 (verified
against the previous chart for both the default values and the CI values).
The four things that fail silently without them:
- A START GATE on gluetun. MediathekArr fetches its ruleset once at start and
never retries; started before the tunnel it answers every Newznab search with
0 results while the container is up, Ready and quiet. Kubernetes cannot order
two regular containers and an initContainer would deadlock, so the wait sits
in front of the image entrypoint: poll gluetun's health server on
127.0.0.1:9999 and exec the entrypoint afterwards; on timeout the container
exits rather than starting blind, because a crash loop is visible and an empty
indexer is not. The startup probe budget (300s) deliberately outlasts the gate
(180s).
- incomplete/ AND complete/ ON THE SHARED DOWNLOADS CLAIM, not in an emptyDir.
An emptyDir counts against the container's ephemeral-storage limit, and one
large download would have the kubelet evict the whole pod with all ten
containers - the trap ytdlp-web-player documents.
- THE COMPLETE PATH SONARR SEES. The claim is mounted at /downloads exactly as
sonarr/radarr mount it, and the chart sets DOWNLOAD_COMPLETE_PATH itself, so
the SABnzbd complete_dir it reports is the same absolute path on both sides.
- TWO PORTS: 5007 (SABnzbd role and UI, primary) and 5008 (Newznab role for
Prowlarr), both declared to gluetun's firewall and both on one Service. The
SABnzbd API is at /download/api - /api on 5007 answers with a misleading
Newznab validation error.
Further details:
- Image pinned by digest (tag + digest): "latest" is a 2024 build, "beta"
moves. Single-arch linux/amd64. The built-in helm-values manager cannot
follow that pair, so renovate.json5 gets the chart's first customManagers
block; verified by dry-run with two fixtures, since a matching and a broken
rule look identical while the pin is current.
- Hardened as measured: non-root, uid/gid from puid/pgid,
readOnlyRootFilesystem: true, all capabilities dropped, no PUID/PGID.
- Probes on measured 200 endpoints (there is no health endpoint): readiness and
startup on /download/, liveness as an exec covering BOTH ports, because one
container runs two processes and a wedged Newznab half is exactly what makes
searches come back empty.
- No ingress by default: the UI links its assets with root-absolute paths, so it
can only be served at the root of a host; on the shared domain that root
belongs to seerr. A sub-path fails the render instead of producing a broken
UI.
- values.yaml and NOTES.txt document the geo MARKER PROCEDURE (not a table of
series), the escape route of a German exit and its price for the whole stack,
the fail_message: null trap, and the HLS question as an open question.
- CI: mediathekarr enabled in ci/freesailarr/test-values.yaml, install timeout
raised to 1200s for the extra image and the gate.
Fixes#245
Deploys matszwe02/ytdlp_web_player, a Flask app that fetches videos with
yt-dlp, transcodes them with ffmpeg and serves them as HLS. It ships no
authentication of any kind, so the chart carries a preconfigured oauth2-proxy
as an optional subchart, in the data path in front of the app.
This is the first chart in the repo with a dependency, so the rule "charts
have no dependencies" falls here. It is replaced in CLAUDE.md and README.md by
a bar: a dependency has to be optional and off by default, upstream's chart is
consumed as it comes (documented where it falls short of our conventions,
never patched), and the price is written down - above all that a subchart is
invisible to everything downstream watching Helm release versions, so a new
proxy release only reaches a cluster when this chart is republished.
CI learns the dependency in five places, not the three the plan expected:
helm lint, package, template and install all exit 1 while charts/ is unfilled,
so ci/helm-dependencies.sh runs before lint and package in validate-charts.yml,
before package in publish-helm-chart.yml, and inside run-install-test.sh,
run-trivy-config.sh and run-quantity-render-test.sh - the last of which renders
every chart on every PR and would have gone red first.
Notable chart decisions, each measured rather than assumed:
- The image has no USER and runs as root by default, but not by necessity: it
runs under an arbitrary uid with readOnlyRootFilesystem and the full
hardening baseline. Only the pinned uid is a deviation, and it is documented.
- Two emptyDirs, each justified separately. /data holds the video cache
(DATA_PATH). /tmp is NOT mounted - measured across metadata extraction, HLS
transcoding, sprite generation and audio extraction, the image never writes
there. What it does write is $HOME/.cache, so /cache is mounted with HOME and
XDG_CACHE_HOME pointed at it; without it yt-dlp re-downloads its JavaScript
challenge-solver from github.com on every extraction.
- The proxy is wired through config.configFile, the only value the subchart
passes through tpl - config.upstreams is emitted with toJson and would carry
a literal {{ .Release.Name }} into the manifest.
- Rendering aborts when this chart's Ingress and the proxy's claim the same
host: ingress-nginx rejects the second one and a Fleet bundle then sits in
ErrApplied until forceSyncGeneration is bumped.
Fixes#214
BentoPDF computes entirely in the visitor's browser; the container only serves
static files through nginx. The chart is therefore close to cyberchef - same
nginx-unprivileged base, uid 101, port 8080, probes on "/" - with four
differences that shaped the design.
Runtime config.json. The one setting reachable at runtime is the tool list.
It is a static file in the middle of the web root, so it is mounted by
subPath: a directory mount would hide the whole application. subPath mounts
are never refreshed by the kubelet, which makes the checksum/config annotation
(#82) load-bearing rather than decorative. bentopdf.config is passed through
verbatim, like searxng's settings, so a key upstream adds tomorrow is a values
change and not a chart change.
Three env switches that would kill the pod. PORT, DISABLE_IPV6 and
ROBOTS_NOINDEX are implemented as "sed -i" against files in the image, run
under "set -e" in /docker-entrypoint.d/, and the base image's entrypoint
aborts when one of them fails. With readOnlyRootFilesystem: true the pod
therefore never starts instead of ignoring the switch - measured in k3d as
CrashLoopBackOff, exit 1, "sed: can't create temp file
'/etc/nginx/nginx.confXXXXXX': Read-only file system". None of the three is
offered as a first-class value; values.yaml names the failure picture and the
escape hatch (env block plus readOnlyRootFilesystem: false).
Writable paths were measured, not copied from the upstream chart.
/etc/nginx/tmp holds the pid file and /var/cache/nginx is where nginx creates
its temp directories at startup - without it the master aborts with
'mkdir() "/var/cache/nginx/client_temp" failed'. /var/run is deliberately not
mounted: the running pod proves it is unnecessary.
Everything else about BentoPDF is a build-time argument and out of reach for
any chart; values.yaml says so explicitly rather than leaving it to be
discovered. Two accepted consequences are named there too: some tools fetch
WASM modules from a public CDN at runtime, and the served pages carry a
canonical link to the vendor's site.
Fixes#174
SearXNG as a stateless Deployment plus a settings ConfigMap, with the four CI
steps a new chart needs: publish workflow, change-app-version choice list,
ci/searxng/ fixtures and a renovate.json5 block.
Three points where the chart is not a cyberchef with another name:
* The image entrypoint writes settings.yml into /etc/searxng only when the
file is missing. The chart always mounts one by subPath, so the copy and
the random-secret sed never run and the container needs no write access
there - which is what allows readOnlyRootFilesystem: true.
* The signing key is a required OnePasswordItem, not a ConfigMap key. It
signs the settings cookie and the image-proxy URLs, and the entrypoint's
self-generating fallback cannot fire here, so without it the instance would
run on the literal "ultrasecretkey" - which webapp.py refuses with exit 1.
* All three probes use /healthz, the only path the bot detection lets through
unconditionally. Measured: with the limiter active, "/" answers 429 while
/healthz still answers 200.
ingress.domain is read a second time outside the Ingress template, for
SEARXNG_BASE_URL, and carries its own required() there - so it stays
mandatory with the Ingress switched off.
The Renovate versioning regex deviates from the plan on the issue and matches
the commit hash WITHOUT capturing it. Renovate parses a "build" group with
Number.parseInt, so a hash becomes the digits leading it; measured on
renovate 41.173.1 and 44.48.0, the planned form reports an image pushed
earlier the same day as an update to a newer one. Reasoning in the block and
in README.
linuxserver/qbittorrent still carries 14.3.9 and 20.04.1 from earlier tagging
schemes. Docker versioning sorts both above every real release, so Renovate
offers them as major updates - PRs #152 and #153 are exactly those, and they
would have to be closed by hand again after every lookup.
They cost more than the noise. Renovate fills the major bucket once, so while a
stray occupies it the genuine major is never offered at all: measured from
currentValue 4.6.4, the unconstrained lookup returns 4.6.7 and 20.04.1, and
never mentions 5.2.3.
Both are excluded by name. A bound such as "<10" would reject a genuine major
sooner or later, whereas two finished artifacts cannot grow and so cannot mask
a release that does not exist yet. A shape-based filter was tried first and
measured out: requiring a non-padded minor drops 20.04.1 and simply promotes
14.3.9 into its place.
Verified with renovate 44.41.0 (dry-run=lookup) against the live registry:
currentValue 5.2.1 -> [5.2.3 (patch)], both strays gone
currentValue 4.6.4 -> [4.6.7 (patch), 5.2.3 (major)]
The second fixture is the guard the nginx rule taught us to run - it proves the
filter does not silently empty the update list. A full run over the real
repository with the real config confirms it in place: qbittorrent offers 5.2.3
only, nginx is unchanged, and no docker dependency carries a skipReason or a
warning.
The chart bundles nine independently versioned upstreams, and eight of them
were already pinned in values.yaml while only gluetun took its tag from
appVersion. That split had no upside: it made gluetun the one image Renovate
could not reach through the built-in helm-values manager, and it implied a
"main app" the stack does not have.
gluetun now carries gluetun.image.repository/tag like every other image, and
the StatefulSet renders it from there. No tag comes from appVersion any more.
Renovate picks all nine up through helm-values - the manager the repo already
relies on for sidecar images - and ci/renovate-bump-chart.sh bumps the chart
version with each update.
Consequences, all documented where they bite:
- renovate.json5 deliberately gets no app-version block for this chart. One
would either duplicate the helm-values updates or rewrite an appVersion that
selects no image. The absence is commented so it does not read as an
oversight.
- freesailarr leaves the change-app-version.yml choice list. That workflow
rewrites appVersion and republishes, which for this chart would change no
deployed image at all.
- appVersion becomes a hand-maintained marker for the gluetun line. Since
renovate-bump-chart.sh bumps the patch level on an unchanged appVersion, a
gluetun update has to be paired with a hand edit of appVersion, or the +up
suffix stops describing what the chart deploys. Noted in Chart.yaml,
values.yaml and README.
The rationale called the alpine switch a proposal from the issue text.
A later comment on #84 commissions it as a check with a defined scope
(nginx.conf compatibility, port 8080, uid 101, /tmp redirection) to be
done alongside the seccompProfile PR for this chart. The outcome is open,
the check is not - the constraint has to survive either way.
The anchored form rejected any tag with a suffix. Docker versioning only
offers candidates carrying the current tag's suffix, so the day the
delegation sidecar moves to a slimmer base image (#84, still open) every
candidate would fail the filter and nginx would stop receiving updates -
without an error, a warning, or a red dashboard.
Verified against currentValue 1.28.0-alpine: anchored form returns
"updates": [] and "warnings": [], the suffix-tolerant form proposes
1.30.4-alpine and still refuses the newer 1.31.x mainline.
nginx numbers its release branches by minor: even is stable, odd is
mainline. PR #139 proposed 1.30.4 -> 1.31.4, which would have moved the
Synapse delegation sidecar onto mainline without that ever being a
decision. allowedVersions now permits even minors only; patch updates
inside the stable branch still come through and still automerge.
The two open Renovate PRs (#138 alpine 3.22 -> 3.24, #139 nginx 1.30.4 ->
1.31.4) never automerged because both are minor updates while the rule
only covered patch and digest. Sidecar images barely move in patch steps,
so patch-only would practically never fire.
Automerging a chart update publishes it, so the install-test can no
longer report failures without blocking: continue-on-error is gone and
issue #22's stabilization phase ends here. Majors stay manual.
Renovate only ever rewrites appVersion or an image tag, so every PR it
opened against a chart directory failed the build-chart version check
(#138, #139). ci/renovate-bump-chart.sh recomputes
version: <semver>+up<appVersion> as a postUpgradeTasks hook: chart major
when the app major moves, chart patch otherwise.
Each chart renders its main image as <repo>:{{ .Chart.AppVersion }}, so
repository and version live in different files and Renovate cannot join
them. The mapping is one customManager per chart in renovate.json5,
deliberately not a comment inside each Chart.yaml - that would touch all
20 chart directories and force 20 publishes for a comment change.
Patch and digest updates automerge after a 3-day minimumReleaseAge once
the pipeline is green; majors arrive one line at a time to match the
stepwise app-major procedure. ci/** stays excluded: the install-test
fixtures pin throwaway infrastructure to major lines on purpose.