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.
helm-charts
Collection of some self-made helm-charts. Each top-level directory is a standalone Helm chart that gets packaged and pushed to a private OCI registry (JK-Registry) via GitHub Actions.
Charts
| Chart | Deploys | Notes |
|---|---|---|
apache-tika |
Apache Tika | Deployment + Service only (no ingress) |
calibre-web-automated |
Calibre-Web Automated | StatefulSet, extra consume-PVC |
cyberchef |
CyberChef | Stateless Deployment |
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 |
outline |
Outline | StatefulSet, separate DB/Redis secrets |
paperless-ngx |
Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap |
patchmon |
PatchMon | Two Deployments (frontend + backend), OIDC secret |
rallly |
Rallly | Deployment, DB/OIDC/SMTP secrets |
samba |
Samba | StatefulSet, users secret + config ConfigMap |
satisfactory |
Satisfactory game server | StatefulSet, no ingress |
teamspeak-server |
TeamSpeak server | StatefulSet, no ingress |
template-chart |
— | Scaffold for new charts (placeholder xxx), never published |
urlaubsverwaltung |
Urlaubsverwaltung | Deployment, DB + OIDC secrets |
voucher-vault |
VoucherVault | StatefulSet, DB + OIDC secrets |
zipline |
Zipline | StatefulSet, DB secret |
Versioning
Chart.yaml uses the scheme version: <chart-semver>+up<app-version> (e.g. 1.4.0+up1.9.2), where appVersion matches the +up suffix (dashes in the app version are normalized to dots in the suffix, e.g. 0.0.36-beta → +up0.0.36.beta).
- Bump the semver part when the chart itself (templates/values) changes.
- The app version is bumped via the
Change App-Versionworkflow, which rewritesversion:/appVersion:and commits directly tomain.
CI/CD (GitHub Actions)
publish-<chart>.yml— one thin workflow per chart; triggers on pushes tomaintouching<chart>/**(or manually) and calls the reusable workflow below. A push to a chart directory onmainpublishes that chart, so the chart version must be bumped in the same change.publish-helm-chart.yml— reusable workflow:helm package, thenhelm pushto the private JK-Registry. Registry credentials come from 1Password Connect (OP_CONNECT_TOKENsecret, items underop://GitHub-Actions/).change-app-version.yml— manually dispatched app-version bump; the chart must be listed in itschart-namechoice list.validate-charts.yml— PR pipeline: detects which charts a PR touches and, for exactly those, runshelm lint --strict,helm package, and a chart-version bump check (template-chartis exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an install-test job (same changed-charts matrix) installs each changed chart into an ephemeral k3d cluster on the runner and requires every workload to become Ready — see below.
Install-test (PR pipeline stage 2)
For every chart a PR changes, ci/run-install-test.sh <chart> runs against a throwaway k3d cluster: it applies the stub OnePasswordItem CRD plus the chart's fixtures, helm installs the chart with its CI values, and waits for all Deployments/StatefulSets to roll out (per-chart timeout). Since probes and security hardening are active on all workloads, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
Fixtures live at the repo root under ci/ (deliberately not inside the chart directories, so they are neither packaged into the chart tgz nor trigger the publish workflows on merge):
ci/common/— fixtures applied for every chart (schema-less stub CRD forOnePasswordItem)ci/<chart>/test-values.yaml— values for the CI install (required for every installable chart)ci/<chart>/fixtures.yaml— dummy secrets with in-cluster connection data, throwaway infrastructure (postgres/redis/dex), static hostPath PVs/StorageClasses where the chart pins a storage class or needs RWX, and seed jobs (e.g.synapse generate)ci/<chart>/settings.env— optional overrides (e.g.INSTALL_TIMEOUTfor slow starters)ci/excluded-charts.txt— charts that cannot run in CI, each with a justification. Currently:satisfactory(multi-GB image + 10GB+ Steam download),teamspeak-server(the TS6 v6.0.0-beta8 image crashes with "The default license has expired" until the appVersion is bumped),template-chart(scaffold with placeholder image)
Fixture images avoid Docker Hub where possible (public.ecr.aws/ghcr.io) to dodge runner rate limits. The job currently runs with continue-on-error: true (stabilization phase); it will be promoted to a required check once it has proven stable across several chart PRs. When adding a new chart, add its ci/<chart>/ fixtures (or an entry in ci/excluded-charts.txt) — the install-test fails if both are missing. The script also runs locally against any existing cluster: k3d cluster create t && ci/run-install-test.sh <chart> && k3d cluster delete t.
Creating a new chart
- Copy
template-chart/to<chart-name>/and replace everyxxxplaceholder: template file names (xxx.deployment.yaml→<app>.deployment.yaml),Chart.yaml, thevalues.yamlkeys, and the.Values.xxx.*references inside the templates. - Add
.github/workflows/publish-<chart-name>.yml(copy an existing one, e.g.publish-outline.yml). - Add the chart name to the choice list in
.github/workflows/change-app-version.yml.
Validate locally with:
helm lint --strict <chart-name>
helm template <chart-name> --set ingress.domain=example.com --set ingress.clusterIssuer=letsencrypt
(required values vary per chart)
For runtime verification (do the workloads actually start, do probes/security settings hold?), use a throwaway local k3d cluster: k3d cluster create <name>, apply a stub CRD for OnePasswordItem plus dummy secrets (and throwaway postgres/redis pods where the app needs them), helm install the chart, watch pod readiness/events, then k3d cluster delete <name>.
Chart conventions
- Resources are named
{{ .Release.Name }}-{{ .Chart.Name }}-<kind>; template files are named<app>.<kind>.yaml(StatefulSets use.sfs.yaml). - Image tags come from
{{ .Chart.AppVersion }}. - Secrets are 1Password
OnePasswordItemCRs: values exposesecrets.<name>SecretRefholding anop://item path; the 1Password operator materializes the Kubernetes Secret in-cluster. - Ingress assumes ingress-nginx (
ingressClassName: nginx) and cert-manager:ingress.clusterIssuerandingress.domainare required values; the TLS secret is namedtls-<release>-<chart>-ingress. enventries invalues.yamlare rendered throughtpl, so values may contain Helm templating;value,secretKeyRefandconfigMapKeyRefforms are supported.- Hardened defaults:
automountServiceAccountToken: false, non-root user,readOnlyRootFilesystem: true, all capabilities dropped, probes and resource requests always set. Pod and container contexts are configurable viasecurityContext.pod/securityContext.containerin each chart's values; writable paths (e.g./tmp, nginx cache dirs, s6-overlay's/run) are emptyDirs. Where an image cannot satisfy the full standard (e.g. Home Assistant must start as root for s6-overlay, paperless needs a root init container to hand over/run), the deviation is minimal and justified with a comment in that chart'svalues.yaml. - Resource requests ship as chart defaults and are user-overridable; limits are computed by a per-chart
_helpers.tplhelper (reference implementation intemplate-chart/templates/_helpers.tpl): an explicitly set limit always wins, missing memory and ephemeral-storage limits default toresources.limitFactor× the request (factor default3), and a CPU limit is only rendered when set explicitly — otherwise CPU stays unlimited (it is compressible, throttling instead of OOM-killing). The helper is duplicated into every chart because charts have no dependencies. - Charts have no dependencies (
charts/directories are empty).