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.
Three review changes to the freesailarr chart.
Ingress: the chart no longer derives hosts as <app>.<ingress.domain> from one
Ingress object. The global ingress.domain is gone; every app carries its own
<app>.ingress.host and renders its own Ingress <release>-<chart>-<app>-ingress
with its own TLS secret tls-<release>-<chart>-<app>-ingress. The hosts are
therefore independent and may live in unrelated domains. The host is required,
but the required() call sits inside the loop over the *enabled* app ingresses,
so switching an app's ingress off must not make the chart demand a host for it.
clusterIssuer, className and annotations stay global defaults and are
overridable per app; an explicitly set per-app value wins (nil check instead of
"default", so an empty override is honoured).
Media: media.<category>.subPath mounts a sub-directory of the claim instead of
its root, default null (claim root = previous behaviour). The same
existingClaim may back all three categories with different subPaths. That is
the point rather than cosmetics: Radarr and Sonarr import by moving, and a move
within one filesystem can be a hardlink instead of a copy - across volume
boundaries every import duplicates the file.
Services: the collective Service with named ports is replaced by one ClusterIP
Service <release>-<chart>-<app>-service per enabled app that listens on a port,
flaresolverr included; recyclarr serves no port in daemon mode and gets none.
Each ingress points at its own app's service. All containers share gluetun's
network namespace, so the services differ only in their port - they provide
stable per-app DNS names, not network separation. The StatefulSet's governing
serviceName no longer names a removed object.
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.
Takes over the four loose manifests in cluster-config/rss-bridge/ as a
proper chart, on the current template-chart standard: strict values
schema, null-safe helpers, computed limits, replicas/scaleDown,
checksum/config, NOTES.txt.
Two things deviate from the repo standard on purpose:
- The Ingress models host AND TLS secret name per entry instead of the
single conventional tls-<release>-<chart>-ingress. The installation
this replaces has grown three separate certificates, one per host.
Renaming them would make cert-manager issue three new ones and leave
each host unreachable until it did - a self-inflicted outage on a
deployment that has been up for over a year. Entries sharing a secret
name are grouped into one tls entry, and omitting the name falls back
to the convention, so consolidating later is a values change.
- Requests and limits are decoupled. The old manifest requested 2Gi of
ephemeral storage, identical to its limit, reserving two gigabytes for
a workload measured at 2.2 MB after fetching all five whitelisted
bridges. The request drops to 64Mi; the limit stays at 2Gi.
Hardening was verified rather than assumed. The image ships no USER and
runs as root by default, but runs fully hardened once its writable paths
are provided: uid/gid 33, readOnlyRootFilesystem, all capabilities
dropped, seccompProfile RuntimeDefault. The writable paths were found by
running the image with a writable rootfs, exercising every bridge and
reading its diff layer - /app/cache, /run, /run/php, /var/lib/nginx.
/tmp did not show up that way, because PHP's opcache creates its lock
file there and unlinks it immediately; it only surfaced when the
hardened container died at startup. Stopping at the first writable path
would have shipped a chart that cannot start.
Probes use the app's own /?action=health, which renders fixed JSON
without touching the cache or any remote site, so neither a scraped site
going down nor an unwritable cache can restart the pod.
Fixes#128
Closes out the schema programme (#68) by moving what the 18 charts taught
into the reference implementation and the binding guidelines.
template-chart:
- values.schema.json as the reference schema: additionalProperties false
on every chart-owned level, pod-spec passthrough left open, and the
reasoning inline in the descriptions so a copy carries it along.
- defaultedDict and subBlock plus the per-block fallbacks for both
security contexts, the three probes and resources, wired into both the
Deployment and the StatefulSet.
- the reference itself violated guideline 1: its env block reads
ingress.domain, so with the Ingress gated off it rendered an empty
value. That read is now required(), and the comment says why - the
template is what people copy, so it has to teach the guard, not the
hole.
README, new section "Values that must not lie" with the five rules:
1. a value read outside its obvious template needs its own guard
(matrix-synapse published '{"m.server": ":443"}' to federation without
it, rendering cleanly and reporting healthy)
2. probes, contexts and resources through the null-safe helper, never
behind an "if" - a guard removes the probe instead of showing a null
3. no liveness on an external dependency where a dependency-free path
exists, otherwise document why on the helper
4. a fallback reproduces THIS chart's defaults, not the repo standard -
calibre's readOnlyRootFilesystem: false is what keeps the runtime uid
at 1000 instead of 911 and existing volume data readable
5. a probe default carries its context - port name, Host header, exec
command - or the probe vanishes (teamspeak) or gets 403 (patchmon)
Plus the two mechanics that are easiest to lose: null reaches schema
validation only for keys WITHOUT a chart default (which is why the helper
and the ["object","null"] types each cover a case the other cannot), and
schemas must be validated against real bundle values - that step caught a
draft of mine that would have broken three live paperless releases.
CLAUDE.md: the new-chart checklist now names the schema and the
adaptation step, since the copied file is chart-specific and wrong until
it is adapted.
freesailarr is not covered: it is not on main yet (#59).
Home Assistant reads configuration.yaml, http-config.yaml and
recorder-config.yaml from a 1Password-backed Secret, mounted with subPath.
Two different gaps hide behind that, and only one of them can be closed.
Repointing secrets.configSecretRef at a different vault item used to leave the
running pod on the old configuration. That is now covered: the pod template
carries checksum/secret-ref over the rendered OnePasswordItem. The annotation
is named apart from checksum/config on purpose - it only promises that the
config source changed, not the config itself - matching the pattern
minecraft-server introduced.
Changing the CONTENT of the item in the vault still does not restart anything,
and cannot: Helm never sees the content, so there is nothing to hash. The
operator's own auto-restart annotation is no way out either - onepassword-
operator 1.12.0 enumerates exactly one workload type in its secret-update
handler (appsv1.DeploymentList) and this workload is a StatefulSet, so the
annotation would be a silent no-op advertising a guarantee it cannot keep. It
is deliberately not set. values.yaml states the limitation and the manual
rollout restart it requires; the README guideline records the operator finding,
the annotation precedence trap, and the checksum/secret-ref pattern so neither
is rediscovered the hard way.
Major bump: runtime behavior changes - changing the secret reference now costs
a restart. For this release the effect is preventive, since it currently runs
at zero replicas via scaleDown.
A changed ConfigMap did not restart any pod in this repo: no chart carries
a checksum/config annotation, so the pod template stays identical and the
running container keeps its old config. With subPath mounts (the usual form
here) the kubelet never refreshes the file either, so the change is silently
ineffective - see #82, where this caused a real outage while every bundle was
green and the pod Ready.
template-chart is the reference implementation, so it gets the annotation on
both pod templates (Deployment and StatefulSet), and the README style guide
gets a new binding section 9 covering the rule, the reason, and the limit for
1Password-backed Secrets.
Major bump: runtime behavior changes - a config change now costs a restart.
Stage 3 of the Fleet migration. Two changes, both breaking, hence the major
bump from 1.0.0 to 2.0.0.
Dated image tags: appVersion moves from the rolling line tag "java8" to the
immutable upstream release tag "2026.8.2-java8". The rolling tags are rebuilt
continuously, so a pod restart could pull a different image than the previous
start without that being recorded anywhere. 2026.8.2 is the newest upstream
release (published 2026-08-23) and exists for all three Java lines.
Whitelist semantics: the chart now wires ENABLE_WHITELIST (and ENFORCE_WHITELIST)
in both positions, so minecraft.whitelist.enabled: false actually switches the
whitelist off. Previously the chart only emitted whitelist variables while
enabled was true and had no way to express "off" - and because server.properties
lives on the PVC, a release that had once written white-list=true kept it
forever. players stays guarded by enabled, since a non-empty WHITELIST enables
the whitelist in the image regardless of ENABLE_WHITELIST.
The default flips from enabled: true to enabled: false. Under the corrected
semantics the old default (enabled with an empty player list) would mean a
server nobody can join; false matches both the image and vanilla Minecraft.
Stage 2 of the Fleet migration for minecraft-server. Major bump: runtime
behaviour changes (probes, hardening) and the values interface breaks
(resource renames, strict schema).
Two real bugs first:
- The container declared port 35565 while the Service targeted 25565. It
only worked because targetPort was numeric. Both are now server.port
(25565) and the Service targets the named port.
- The download-serverpack init container was guarded by the map
.Values.curseforgeServerpack instead of .enabled, so it rendered for
every release and silently no-opped on an empty wget URL.
Conventions:
- Resources renamed to <release>-<chart>-<kind>, templates to
<app>.<kind>.yaml. The PVC deliberately keeps its historical name -
renaming it would provision a new, empty volume and orphan the worlds.
- Probes (liveness/readiness/startup) via the image's own mc-health, so
Ready means the server answers a server-list ping. Startup budget 20 min.
- Hardened, values-configurable security contexts: non-root uid 1000,
read-only root filesystem with an emptyDir at /tmp, all caps dropped.
- Resources from values with the limitFactor helper; an unset memory
request is derived from the JVM heap so the two cannot drift apart.
- Additive env block rendered through tpl, pinned init image, storage
class as a value, RCON wiring only when actually enabled.
- curseforge/ftb/curseforgeServerpack are now mutually exclusive.
- minecraft.version is pinned to the Java line instead of LATEST.
- The mc-router auto-discovery annotation is gone; the service type it
implicitly switched is now the explicit value server.service.type.
Schema pilot (#68): values.schema.json with additionalProperties false on
every chart-owned level, rejecting all three known dead keys.
Imports the chart from cluster-config functionally unchanged, wires it into
the publish/validate/install-test pipelines and publishes it as the first of
three Java lines (0.4.0+upjava8).
The only deviation from the source chart is the chart-global scaleDown flag
plus a server.replicas value, following the satisfactory pattern: two of the
three servers stand at 0 replicas in the cluster, which the chart could not
express, and Fleet would otherwise start them on reconcile. The default
(scaleDown: false) still renders replicas: 1, so the rendering of the running
server is unchanged - verified byte-identical against the three real values
files for all three Java lines.
Known deficiencies (hardening, probes, naming scheme, hardcoded values,
missing icon, the misleading mc-router auto-discovery annotations) are left
untouched on purpose and are scheduled for stage 2.
Stage 2 of the Fleet migration for minecraft-router: bring the chart up to the
repo style guidelines. Chart 0.1.1+up1.25.1 -> 1.0.0+up1.25.1 (major: resource
renames, mappings becomes required, probes and security hardening change the
runtime behavior; appVersion unchanged).
- Resources renamed to {{ .Release.Name }}-{{ .Chart.Name }}-<kind>, templates
to <chart>.<kind>.yaml; the deployment label now matches its own selector.
- mappings is required and must not be empty (the old default rendered
"<nil>": "<nil>:25565"); every entry needs domain and internalUrl.
- Ports come from values: server.port (router listen port, also passed as
--port), server.backendPort (default backend port, overridable per mapping),
server.apiPort (--api-binding, used by the probes). Defaults stay 25565 /
25565 / 8080, server.outwardPort keeps its 25565 default and comment.
- _helpers.tpl added (resources/limitFactor and replicas/scaleDown helpers),
resources come from values, no CPU limit.
- replicas value plus the chart-global scaleDown flag.
- Additive env block rendered through tpl (value / secretKeyRef /
configMapKeyRef).
- Values-configurable securityContext.pod / .container with hardened defaults:
non-root uid/gid 10000, readOnlyRootFilesystem, no privilege escalation, all
capabilities dropped.
- Liveness, readiness and startup probes on the mc-router API: GET /routes with
an "Accept: application/json" header (the API router matches on that header
and answers 404 without it).
- icon added to Chart.yaml.
- CI fixture supplies mappings; README chart table entry updated.
Takes the existing mc-router chart over from cluster-config as-is so the
deployment can move to Fleet without any change to the rolled-out state.
Convention alignment follows in stage 2, the app update in stage 3.
The only deviation from "unchanged": Chart.yaml carried name:
minecraft-server (copy-paste from the sister chart). The publish job pushes
by the glob <directory>-*.tgz while helm package names the archive after the
Chart.yaml name, so without this correction the push finds no file.
Version 0.1.1+up1.25.1 - chart semver stays at 0.1.1, the +up suffix is repo
policy and does not affect rendering.
Also wires up CI: publish workflow, change-app-version choice list,
install-test values (CI-only node-port override, since 25565 is outside the
k3d default range) and the README chart table.
README.md gains a comprehensive 'Chart style guidelines' section - the
binding end state of the conventions overhaul (#32, batches 0-4) for all
current and future charts: naming (incl. the PVC-name exception and the
StatefulSet migration pattern), versioning scheme and semver rules,
image pinning, the ingress standard (domain + clusterIssuer strictly
required without defaults, className default nginx, annotations merge,
TLS secret name, homeassistant exception), security hardening with
mandatory documentation of deviations, probes, the auto-limits scheme,
the secrets.<name>SecretRef convention, tpl-rendered env blocks,
scaling, and the icon/lint/CI-fixture/no-dependencies rules.
The Versioning section now points there and documents that app-version
updates go through PRs. CLAUDE.md's conventions section is aligned as a
compact summary referencing the README guide.
Establish the flexible ingress standard from #32 in template-chart and
document it in README/CLAUDE.md: ingress.domain and ingress.clusterIssuer
stay required, new ingress.className value (default nginx) replaces the
hardcoded ingressClassName, new optional ingress.annotations are merged
with the cert-manager cluster-issuer annotation, TLS secret name
unchanged (tls-<release>-<chart>-ingress).
Apply the standard to cyberchef and patchmon (the two charts without an
own batch-3 major PR) and make the patchmon probes values-configurable
(server + guacd, rendered through tpl so the default Host header keeps
following ingress.domain). Defaults render semantically identical to the
previous hardcoded output.
cyberchef 5.0.1 -> 5.1.0, patchmon 5.0.1 -> 5.1.0 (minor, additive).
- Add icon: to all 17 Chart.yaml (official project icons); removes the
last helm lint --strict INFO finding across the repo
- Rename template files to the <app>.<kind>.yaml scheme (apache-tika,
gotenberg, teamspeak-server PVC, paperless-ngx configmap) - pure
git mv, no content changes
- template-chart: fix missing space in {{ .Chart.Name}} and rename the
ConfigMap resource -config-map -> -configmap (chart is never published)
- rallly: fix typos in required messages (rallySecret -> ralllySecret,
databseSecret -> databaseSecret)
- README: document the deliberate PVC-name exception from the naming
scheme (renames would provision new empty volumes)
- Patch-bump all 16 published charts
Rendered resources are byte-identical for all published charts (only
helm's '# Source:' comments change for the renamed files); the only
render change is the template-chart ConfigMap name.
Part of #32
The delegation nginx image was hardcoded as nginx:1.28.3 in the
deployment template. It is now rendered from the new values
matrix.delegation.image.repository / matrix.delegation.image.tag,
defaulting to nginx:1.30.4 (current stable line, tag verified against
Docker Hub). README documents that auxiliary/sidecar images are pinned
via values and reviewed with every app-version update round.
Chart version: 4.1.1+upv1.159.0 -> 4.2.0+upv1.159.0 (minor: additive
values change).
Nine non-critical app-version bumps, each with a chart patch bump:
- apache-tika 3.2.3.0-full -> 3.3.1.0-full
- gotenberg 8.26.0 -> 8.36.0
- matrix-synapse v1.149.1 -> v1.159.0
- rallly 4.8.1 -> 4.12.3
- samba 4.23.5 -> 4.23.10
- urlaubsverwaltung 6.5.0 -> 6.7.0
- voucher-vault 1.27.4 -> 1.30.1
- zipline 4.6.4 -> 4.7.0
- teamspeak-server v6.0.0-beta8 -> 6.0.0-beta12.1 (new tag format has no v prefix)
Release notes of every chart were checked for environment-variable
changes; none of the charts needs an env adjustment.
The teamspeak-server install-test is reactivated: beta12.1 ships a
valid default license, so the exclusion is removed, CI fixtures are
added under ci/teamspeak-server/, and the install-test was verified
in a local k3d cluster. README list of excluded charts updated.
Every Deployment/StatefulSet now takes its replica count from a replicas
value (default 1, explicit 0 allowed) via a new per-chart <chart>.replicas
helper; the helper uses a kindIs "invalid" nil check so an explicit 0 is
not swallowed by Helm's default function. A chart-global scaleDown flag
(default false) takes precedence: it renders every workload with
replicas: 0 and suspends the paperless-ngx consume CronJob, stopping the
app without uninstalling it (PVCs, Secrets, Services and Ingress remain).
Defaults leave the rendered manifests byte-identical to main, so all
published charts get a minor version bump; the template-chart carries the
reference helper implementation and stays unversioned.
Fixes#25
Every chart now renders its container resources through a duplicated
_helpers.tpl helper (reference implementation in template-chart): requests
ship as chart defaults, missing memory and ephemeral-storage limits are
computed as resources.limitFactor x the request (default 3, satisfactory
keeps 2 to preserve its previously published 8Gi limit), an explicitly set
limit always wins, and a CPU limit is only rendered when set explicitly.
Fixed limit defaults are removed from all values.yaml files; teamspeak-server
gains a values-driven resources block instead of hardcoded template values.
Major version bump for all published charts.
New install-test job in validate-charts.yml: for exactly the charts a PR
changes, install them into an ephemeral k3d cluster (pinned k3d v5.9.0,
k3s v1.35.5-k3s1) and require every Deployment/StatefulSet to become
Ready. Runs non-blocking (continue-on-error) during the stabilization
phase.
Fixtures live at the repo root under ci/ (not inside chart directories,
so they neither trigger publishes on merge nor get packaged): a stub
OnePasswordItem CRD, per-chart test values and fixtures (dummy secrets
with in-cluster DSNs, throwaway postgres/redis/dex, synapse-generate
seed job, static hostPath PVs where charts pin storage classes or need
RWX), plus a shared ci/run-install-test.sh harness that also runs
locally.
Excluded with documented reasons (ci/excluded-charts.txt): satisfactory
(multi-GB image), teamspeak-server (TS6 beta license expired, image
crashes on start), template-chart (placeholder image).
All 14 non-excluded charts verified locally in k3d using only the
committed fixtures.
Add securityContext hardening (runAsNonRoot, readOnlyRootFilesystem,
capabilities drop ALL, allowPrivilegeEscalation false) to the 10 workloads
that lacked it: apache-tika, cyberchef, gotenberg, homeassistant,
matrix-delegation-nginx, paperless-server, paperless-consume cronjob
(plus automountServiceAccountToken false), patchmon-frontend,
urlaubsverwaltung and zipline. All contexts are configurable via
securityContext.pod / securityContext.container in values.yaml; writable
paths are provided as emptyDirs. Image-forced deviations (Home Assistant
root start for s6-overlay + NET_RAW for discovery, paperless root init
container for /run ownership) are documented in the charts' values.yaml.
Major version bump for every changed published chart.
The chart is no longer needed. Removes the chart directory, its publish
workflow, and its row in the README chart table. Already published
versions remain in the registry.
Fixes#7