Every Helm chart & raw manifest running in the home Kubernetes cluster, plus the single ApplicationSet that turns them into ArgoCD Applications. 57 apps across 36 namespaces, one Talos cluster.
The path is the configuration. apps/<namespace>/<name>/ gives ArgoCD everything
it needs: the second path segment is the destination namespace, the third is both
the Application name & the Helm release name.
apps/app-media/sonarr/ -> Application "sonarr" in namespace "app-media"
apps/kube-system/headlamp/ -> Application "headlamp" in namespace "kube-system"
apps/app-firebin/firebin-api/ -> Application "firebin-api" in namespace "app-firebin"
appsets/apps.yaml holds the ApplicationSet that does this. It runs a git
directory generator over apps/*/*, so adding a directory adds an app & there's
no second file to keep in sync. A directory generator rather than a files
generator on Chart.yaml, because cloudflare-ddns & unpoller are raw manifest
directories with no chart at all.
Two rules on that ApplicationSet are load-bearing:
applicationsSync: create-update lets the controller create & update Applications
but never delete one. preserveResourcesOnDeletion: true makes it strip
resources-finalizer.argocd.argoproj.io from every Application it generates. Both
exist because no PVC in this repo sets helm.sh/resource-policy: keep, so before
this a deleted Application took its PersistentVolumes with it. Deleting a
directory now leaves the Application running until you remove it yourself:
kubectl delete application <name> -n core-argocdcloudnative-pg is the one app with a per-app override. Its CRDs exceed the
262,144-byte annotation limit that client-side apply relies on, so a second
generator matches only that directory & sets ServerSideApply=true.
| Pattern | Used for | Example |
|---|---|---|
app-<name> |
ArgoCD-managed apps | app-wikijs, app-media |
core-<name> |
Tofu-managed infrastructure | core-argocd, core-longhorn |
| Standard names | System components | cert-manager, kube-system |
A namespace can hold several apps. app-media holds nine, app-ai six,
app-firebin four.
This repo holds charts & the ApplicationSet. It does not hold the ArgoCD install.
The argo-cd Helm release, the homelab-gitops AppProject & Application/app-bootstrap
are OpenTofu resources in ~/Documents/terraform/talos-cluster/core-argocd.tf,
with local state. app-bootstrap points at appsets/ in this repo & is what
applies the ApplicationSet. If you need to change the bootstrap Application,
that's the file, & use -target so a plan doesn't drag in the whole cluster.
Until 2026-08-13 there was a second repo, homelab-config, holding one
app-config.json per app. Its only real content was the destination namespace,
which the path now carries. Two of its four keys did nothing: replace was
false in all 57 files, & createNamespace was read through
{{ or .createNamespace true }}, which returns true for or false true, so the
five apps that set it to false had been getting CreateNamespace=true for
years.
apps/<namespace>/<name>/
├── Chart.yaml # chart metadata & upstream dependency
├── Chart.lock # dependency lock (33 of 55 charts have one)
├── values.yaml # configuration overrides
├── templates/ # extra resources: sealed secrets, CNPG clusters, ingresses
├── charts/ # vendored dependency tarballs, tracked in git
└── README.md # app notes
appsets/apps.yaml # the ApplicationSet
charts/*.tgz is committed, not ignored. 46 tarballs are tracked. Be aware that
11 of them are older than what their Chart.yaml declares: traefik vendors
35.4.0 against a declared 39.0.7, homarr vendors 5.3.0 against 8.16.1. ArgoCD
re-resolves at sync time, so the committed tarball is what a local helm template
uses & what a reviewer reads, & the two disagree. cribl-edge declares a floating
^4.12.1, which is the only chart here whose render isn't reproducible.
Most apps wrap the FireLabs Helm Common Library,
a fork of the k8s-at-home common library, & put everything in values.yaml.
# Chart.yaml
dependencies:
- name: common
version: 5.0.3
repository: https://fireball1725.github.io/firelabs-helm-common/# templates/common.yaml
{{ include "common.all" . }}audiobookshelf & lazylibrarian still point at the old k8s-at-home repo at
4.5.2.
Resource names come from the release name, which is the directory name. 12 charts
pin global.fullnameOverride so their names don't depend on it; the rest inherit.
Renaming a directory therefore renames every Deployment, Service and PVC the
chart owns, which orphans the volume. Don't rename a directory to move an app
between namespaces; that isn't what the layout is for.
| App | Description |
|---|---|
| Home Assistant | Smart home hub |
| Node-RED | Flow-based automation engine |
| ESPHome | ESP device firmware management |
| Zigbee2MQTT | Zigbee device bridge |
| Mosquitto | MQTT broker |
| Matter Server | Matter/Thread controller |
| Music Assistant | Multi-room audio server |
| Homebridge | HomeKit bridge |
| App | Description |
|---|---|
| Kube Prometheus Stack | Prometheus, Grafana, Alertmanager |
| Uptime Kuma | Uptime monitoring & status page |
| Kuma Ingress Watcher | Registers ingresses with Uptime Kuma |
| Unpoller | UniFi network metrics exporter |
| Cribl Edge | Log & metric collection agent |
| App | Description |
|---|---|
| Radarr | Movie library management |
| Sonarr | TV series management |
| Lidarr | Music library management |
| Prowlarr | Indexer aggregator |
| SABnzbd | Usenet downloader |
| Pinchflat | YouTube channel archival |
| Tautulli | Plex analytics |
| Audiobookshelf | Audiobook & podcast server |
| LazyLibrarian | Book library management |
| Kometa | Plex metadata & collections |
| App | Description |
|---|---|
| CloudNative-PG | PostgreSQL operator |
| Cert Manager | TLS certificate automation |
| Traefik | Ingress controller |
| Error Pages | Custom Traefik error pages |
| Tailscale | Mesh VPN |
| Metrics Server | Kubernetes resource metrics |
| Blocky | DNS-based ad blocking |
| Cloudflare DDNS | Dynamic DNS updater |
| App | Description |
|---|---|
| Open WebUI | Web UI for local LLMs |
| Ollama | Bridge to the LLM host at 10.0.1.191 |
| Ollama Admin | Model management UI |
| Openclaw | AI agent gateway (Discord) |
| ha-mcp | Home Assistant MCP server |
| Basic Memory | Persistent note store over MCP |
| Basic Memory Viewer | Web UI for Basic Memory |
| App | Description |
|---|---|
| FireBin API | Electronics inventory backend |
| FireBin Web | Inventory frontend |
| FireBin MCP | Inventory MCP server |
| FireBin KiCad | KiCad HTTP library |
| Librarium API | Book catalogue backend |
| Librarium Web | Catalogue frontend |
| Librarium MCP | Catalogue MCP server |
| App | Description |
|---|---|
| Headlamp | Kubernetes web UI |
| Homarr | Homelab dashboard |
| Homer | Static homepage |
| Wiki.js | Documentation wiki |
| ttyd | Web-based terminal |
| ssh-to-go | Browser SSH gateway |
| Terminus | Terminal workspace |
| n8n | Workflow automation |
| Spoolman | Filament inventory |
| Bambuddy | Bambu printer bridge |
| PC Express MCP | Grocery ordering MCP server |
| Renovate | Dependency update bot |
Create the directory under the namespace it belongs in. That's the whole registration step; there's no second repo & no config file.
mkdir -p apps/app-<name>/<name>Chart.yaml:
apiVersion: v2
name: <name>
type: application
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: <chart-name>
version: <chart-version>
repository: https://<chart-repo-url>/values.yaml:
<chart-name>:
env:
TZ: "America/Toronto"
persistence:
config:
enabled: true
storageClassName: "longhorn"
size: 10Gi
ingress:
enabled: true
hosts:
- host: <name>.k8s.firekatt.ca
paths:
- path: /
pathType: PrefixThen helm dependency update, commit, push. The ApplicationSet picks it up on
the next refresh.
Sealed Secrets, so encrypted material can sit in a public repo & decrypt only
inside the cluster. The controller is sealed-secrets-controller in kube-system.
kubectl create secret generic <name> \
--namespace=<namespace> \
--from-literal=<key>=<value> \
--dry-run=client -o yaml | \
kubeseal --controller-name sealed-secrets-controller \
--controller-namespace kube-system \
-o yaml > apps/<namespace>/<name>/templates/sealed-secret.yamlSeal secrets, don't generate them. ArgoCD caches rendered manifests per
(revision, path), so a template using randAlphaNum mints a new value on every
commit to this repo while the running pods keep whatever they started with.
terminus did exactly that for 94 syncs before it was sealed on 2026-08-13. Its
APP_SECRET is read through secretKeyRef into an env var, which resolves at pod
start, so nothing broke until a pod restarted & logged everyone out. lookup is
not a way around this: the repo-server has no cluster access & returns empty.
See SECRETS.md for the full workflow.
| Platform | Talos Linux |
| Kubernetes | v1.33+ |
| ArgoCD | v3.0.3, namespace core-argocd |
| Storage | Longhorn, namespace core-longhorn |
| Ingress | Traefik v3 |
| Load balancer IP | 10.0.1.100 |
# force a hard refresh on one app
kubectl -n core-argocd patch application <name> --type merge \
-p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}'
# anything not healthy
kubectl get applications -n core-argocd --no-headers | grep -v 'Synced *Healthy'
# pods that aren't running
kubectl get pods -A --field-selector status.phase!=Running,status.phase!=Succeeded
# what the ApplicationSet would generate, before you merge
argocd appset generate appsets/apps.yaml -o yaml
# long-lived service account token
kubectl create token <service-account> -n <namespace> --duration=87600hhelm repo add firelabs https://fireball1725.github.io/firelabs-helm-common/
helm repo update
helm dependency update apps/<namespace>/<name>
helm search repo <chart-name> --versions | head -10