···11-# 🪐 Charts
11+# Charts
2233In-tree Helm charts deployed by [`../helmfile.yaml`](../helmfile.yaml).
4455-App charts share a library chart (`common/`); their `templates/*.yaml` files
66-are one-line includes of `common.deployment` / `common.service` / etc., and
77-all per-app config lives in `values.yaml`.
55+Most app charts use explicit Kubernetes manifests. Helm is used for light
66+substitution, mostly `.Chart.Name` and secret values passed by helmfile. Avoid
77+shared Deployment/Service/PVC helpers; app-specific behavior should stay visible
88+in the app chart.
8999----
1010+## Layout
10111111-## 📂 Layout
1212-1313-```plaintext
1212+```text
1413charts/
1514├── aly-codes/ # Static site (aly.codes)
1515+├── audiobookshelf/ # Audiobook library with rclone-mounted media
1616├── bluesky-pds/ # Reference atproto Personal Data Server
1717-├── cert-manager-issuers/ # Let's Encrypt ClusterIssuer + wildcard Certificates (Cloudflare DNS-01)
1717+├── cert-manager-issuers/ # Let's Encrypt ClusterIssuer + wildcard Certificates
1818├── cluster-tls/ # Cloudflare Origin TLS Secrets per domain
1919-├── common/ # Library chart with shared partials
2019├── external-routes/ # Ingress + Service + EndpointSlice for off-cluster targets
2120├── forgejo/ # Git hosting (git.aly.codes)
2222-├── longhorn-creds/ # B2 backup-target Secret + recurring backup job + UI ingress
2323-├── morsels/ # atproto pastebin (morsels.blue)
2121+├── forward-auth/ # Per-app traefik-forward-auth frontends
2222+├── immich/ # Photo library + ML + app-specific Postgres
2323+├── longhorn-creds/ # B2 backup Secret + recurring backup job + UI ingress
2424+├── paperless/ # Document management with rclone media mount
2425├── pg-shared/ # CloudNativePG cluster using local-path replicas
2525-├── tranquil-pds/ # TRanquil atproto Personal Data Server
2626-├── uptime-kuma/ # Uptime monitoring + status pages
2727-├── vaultwarden/ # Bitwarden-compatible vault
2828-└── watsup/ # Homelab dashboard (cute.haus)
2626+└── ...
2927```
30283131----
2929+## Chart Style
32303333-## 🔐 Secret flow
3131+Prefer direct manifests:
34323333+```yaml
3434+apiVersion: apps/v1
3535+kind: Deployment
3636+metadata:
3737+ name: { { .Chart.Name } }
3838+ labels:
3939+ app: { { .Chart.Name } }
4040+spec:
4141+ selector:
4242+ matchLabels:
4343+ app: { { .Chart.Name } }
4444+ template:
4545+ metadata:
4646+ labels:
4747+ app: { { .Chart.Name } }
4848+ spec:
4949+ automountServiceAccountToken: false
5050+ enableServiceLinks: false
5151+ containers:
5252+ - name: { { .Chart.Name } }
5353+ image: example/app:1.0.0@sha256:...
3554```
3636-secrets/foo.yaml (SOPS-encrypted, multi-recipient age)
3737- │
3838- ▼ vals reads ref+sops:// at render time
3939-values/foo.yaml (plain yaml of ref+sops://... refs)
4040- │
4141- ▼ helmfile passes it as values: to the release
4242-chart's values.yaml (.Values.secret.* now plaintext during render)
4343- │
4444- ▼
4545-common.secret partial → kind: Secret with stringData
4646- │
4747- ▼
4848-deployment.envFrom → env vars in the container
5555+5656+Use this pod-spec order when practical:
5757+5858+```yaml
5959+automountServiceAccountToken: false
6060+enableServiceLinks: false
6161+terminationGracePeriodSeconds: 30
6262+securityContext:
6363+tolerations:
6464+hostNetwork:
6565+dnsPolicy:
6666+nodeSelector:
6767+imagePullSecrets:
6868+topologySpreadConstraints:
6969+initContainers:
7070+containers:
7171+volumes:
4972```
50735151-`vals` resolves `ref+sops://` URLs at render time using whichever age key
5252-is at `~/.config/sops/age/keys.txt` (run `just sops-bootstrap` once on a
5353-new machine to derive that from the SSH host key).
7474+Only include fields that the app actually needs. Do not add shared helpers just
7575+to make every chart identical.
54765555----
7777+## Templating Rules
7878+7979+Keep these:
8080+8181+- `.Chart.Name` for names, labels, selectors, and PVC names.
8282+- `if .Values.secret` around Secret manifests populated by vals/SOPS.
8383+- Small `range` loops for charts that are naturally data-driven.
56845757-## 🆕 Add a new app
8585+Avoid these:
58865959-```bash
6060-just new-app <name>
6161-```
8787+- Common Deployment/Service/PVC helper templates.
8888+- Values that model arbitrary pod specs, containers, sidecars, volumes, or env.
8989+- Large `_helpers.tpl` files for simple app charts.
62906363-Then:
9191+## Data-Driven Exceptions
64926565-1. Edit `charts/<name>/values.yaml` — image, ports, env, ingress routes,
6666- persistence.
6767-2. Add a release block to `helmfile.yaml`:
6868- ```yaml
6969- - name: <name>
7070- namespace: default
7171- chart: ./charts/<name>
7272- ```
7373-3. If the app needs secrets:
7474- - `just sops-edit <name>.yaml` — write the encrypted file
7575- - Create `values/<name>.yaml` with `ref+sops://../secrets/<name>.yaml#/...`
7676- refs for each key (path is relative to `k8s/`, where helmfile runs)
7777- - Add `values: [values/<name>.yaml]` to the helmfile release
7878-4. `helmfile -l name=<name> apply`
9393+Some charts intentionally render repeated resources from values:
79948080----
9595+- **`forward-auth`** renders one auth Deployment/Service/Ingress/Middleware set
9696+ per `.Values.apps` entry.
9797+- **`external-routes`** renders off-cluster Service, EndpointSlice, and Ingress
9898+ resources from `.Values.routes`.
9999+- **`pg-shared`** renders CNPG roles/databases and backup resources from chart
100100+ values.
101101+- **`cluster-tls`** and **`cert-manager-issuers`** render repeated TLS or
102102+ certificate resources from values.
811038282-## 🧱 Library chart
104104+Those are data charts rather than app workload charts, so a little looping is
105105+acceptable there.
831068484-App templates are thin includes:
107107+## Secret Flow
851088686-```yaml
8787-# charts/<app>/templates/deployment.yaml
8888-{ { - include "common.deployment" . } }
109109+```text
110110+secrets/foo.yaml SOPS-encrypted, multi-recipient age
111111+ |
112112+ v
113113+values/foo.yaml ref+sops://... refs read by vals
114114+ |
115115+ v
116116+helmfile values: passes plaintext values during render
117117+ |
118118+ v
119119+templates/secret.yaml renders a chart-local Secret
120120+ |
121121+ v
122122+deployment envFrom app consumes the Secret
89123```
901249191-The library defines `common.deployment`, `common.service`, `common.ingress`,
9292-`common.pvc`, `common.secret`. See [`common/README.md`](common/README.md) for
9393-each partial's values reference.
9494-9595----
125125+`vals` resolves `ref+sops://` URLs at render time using the local age key.
961269797-## 🚫 Charts that don't use `common/`
127127+## Adding An App
981289999-These have unique enough shape that the library wouldn't help:
129129+1. Create `charts/<name>/Chart.yaml`.
130130+2. Add explicit templates for only the resources the app needs, usually
131131+ `deployment.yaml`, `service.yaml`, `ingress.yaml`, optional `pvc.yaml`, and
132132+ optional `secret.yaml`.
133133+3. Put app-specific behavior directly in those manifests.
134134+4. If the app needs secrets, create `secrets/<name>.yaml` and
135135+ `values/<name>.yaml`, then add that values file to the helmfile release.
136136+5. Add the release to `../helmfile.yaml`.
137137+6. Render before applying:
100138101101-- **`cluster-tls`** — renders one `kubernetes.io/tls` Secret per entry in
102102- `.Values.secret`, sourced from `secrets/cluster-tls.yaml`.
103103-- **`pg-shared`** — a CNPG `Cluster` using `local-path` volumes for Postgres
104104- data. CNPG provides HA with streaming replication across instances; B2
105105- backups and WAL archiving cover recovery. A `longhorn-pg` StorageClass is
106106- rendered for experiments or a future migration, but the live cluster does not
107107- use it. Apps that need a database get a role + database created manually in
108108- this cluster; there's no per-app provisioning yet.
109109-- **`external-routes`** — for each entry in `.Values.routes`, renders a
110110- `Service` + `EndpointSlice` + `Ingress` pointing at an external IP
111111- (typically a Tailscale IP for a service running on jubilife or eterna).
112112- Supports both traefik and tailscale ingress classes.
113113-- **`cert-manager-issuers`** — one `ClusterIssuer` (Let's Encrypt + Cloudflare
114114- DNS-01) plus one wildcard `Certificate` per entry in `.Values.wildcards`.
115115- Each cert lands as a Secret apps reference via `tlsSecret` in ingress routes.
116116-- **`longhorn-creds`** — the B2 credentials Secret that Longhorn references via
117117- `defaultBackupStore.backupTargetCredentialSecret`, plus a daily `RecurringJob`
118118- for the `default` volume group and an Ingress exposing the Longhorn UI on the
119119- tailnet. Must apply before `longhorn` (the `needs:` chain enforces this).
139139+```bash
140140+helm template <name> charts/<name>
141141+helmfile -l name=<name> apply
142142+```
+1-1
k8s/charts/cert-manager-issuers/values.yaml
···22 email: aly@aly.codes
33 server: https://acme-v02.api.letsencrypt.org/directory # use https://acme-staging-v02.api.letsencrypt.org/directory for testing
4455-# Templated by common.secret pattern; populated from values/secrets/cert-manager.yaml.
55+# Populated from values/secrets/cert-manager.yaml.
66cloudflare:
77 apiToken: ""
88
-5
k8s/charts/common/Chart.yaml
···11-apiVersion: v2
22-name: common
33-description: Shared template helpers for cute.haus app charts.
44-type: library
55-version: 0.1.0
-85
k8s/charts/common/README.md
···11-# common (library chart)
22-33-Shared template partials for cute.haus app charts. App charts depend on
44-this and include the partials they need from their own `templates/*.yaml`:
55-66-```yaml
77-{ { - include "common.service" . } }
88-```
99-1010-Deployments are hand-written per app — no shared template. All partials
1111-read from the consumer chart's `.Values` and use `.Chart.Name` for names.
1212-1313----
1414-1515-## Partials
1616-1717-| Define | Renders | Conditional on |
1818-| -------------------------- | --------------------------------------- | ----------------------------- |
1919-| `common.service` | `v1` Service | always |
2020-| `common.ingress` | `networking.k8s.io/v1` Ingress | `.Values.ingress.enabled` |
2121-| `common.pvc` | PVC with `helm.sh/resource-policy:keep` | `.Values.persistence.enabled` |
2222-| `common.secret` | Opaque Secret (envFrom target) | `.Values.envFromSecret` set |
2323-| `common.tailnetIngress` | Tailscale Ingress | `.Values.tailnet.enabled` |
2424-| `common.tailnetProxyClass` | Same-node Tailscale ProxyClass | `.Values.tailnet.enabled` |
2525-2626-### Service
2727-2828-```yaml
2929-service:
3030- type: ClusterIP # default ClusterIP
3131- ports:
3232- - name: http
3333- port: 80
3434- targetPort: 80 # defaults to .port; can be int or named ("http" → ports[].name)
3535- nodePort: 8282 # optional; only used when type=NodePort
3636-```
3737-3838-### Ingress
3939-4040-```yaml
4141-ingress:
4242- enabled: true
4343- className: traefik
4444- routes:
4545- - host: example.com
4646- aliases: [www.example.com] # optional; additional hosts under same TLS
4747- tlsSecret: example-com-tls # name of an existing kubernetes.io/tls Secret
4848-```
4949-5050-The ingress backend always points at the chart's own Service on
5151-`service.ports[0].port`.
5252-5353-### Tailnet Ingress
5454-5555-```yaml
5656-tailnet:
5757- enabled: true
5858- hostname: example # defaults to .Chart.Name
5959- ingressName: example-tailnet # optional
6060- serviceName: example # defaults to .Chart.Name
6161- servicePort: 80 # defaults to service.ports[0].port if present
6262- proxyClass:
6363- name: example-same-node # optional
6464- affinity:
6565- matchLabels: # defaults to app: <chart name>
6666- app: example
6767- namespaces: [default] # defaults to release namespace
6868-```
6969-7070-Renders a Tailscale `Ingress` plus a `ProxyClass` that requires the
7171-operator-managed proxy pod to schedule on the same node as pods labeled
7272-`app: <chart name>`. App charts should include both partials from their own
7373-templates.
7474-7575-### Secret
7676-7777-```yaml
7878-envFromSecret: foo-env # name of the Secret to render
7979-secret: # filled by vals from values/<chart>.yaml
8080- KEY: value
8181-```
8282-8383-Renders one `Opaque` Secret with the contents of `.Values.secret` as
8484-`stringData`. The chart's `templates/secret.yaml` is just
8585-`{{- include "common.secret" . }}`.