Skip to content

Latest commit

 

History

242 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Homelab Applications

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.

How an app becomes an Application

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-argocd

cloudnative-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.

Namespace conventions

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.

Where the rest of it lives

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.

Directory layout

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.

Common library

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.

Applications

Home automation

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

Monitoring

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

Media

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

Infrastructure

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

AI

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

FireBin & Librarium

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

Dashboards & tools

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

Adding an application

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: Prefix

Then helm dependency update, commit, push. The ApplicationSet picks it up on the next refresh.

Secrets

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.yaml

Seal 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.

Cluster

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

Commands

# 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=87600h

Dependency management

helm 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

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages