11 KiB
CLAUDE.md — Homelab Integration Guide (Example / Reusable Template)
This is a sanitized template. Copy to
CLAUDE.md, fill in your own node IPs/hostnames/secrets, and delete this notice. Nothing in this file should contain real credentials, real IPs beyond illustrative examples, or anything that would matter if this file became public. It's meant to be shared across homelabs running similar hardware/topology (3-node bare-metal Talos Kubernetes + ArgoCD GitOps), not just this one.
Homelab: A bare-metal N-node Kubernetes cluster running Talos Linux with a full observability stack, SSO via Authentik, secret management via Vault, and CI/CD infrastructure (self-hosted git forge + Argo CD).
Cluster Topology (adjust to your hardware)
| Node | IP | Zone | Scheduling | Storage |
|---|---|---|---|---|
<node-1> |
<ip> |
az-a | schedulable (all workloads) | sole storage node (if single-node storage) |
<node-2> |
<ip> |
az-b | dedicated (NoSchedule) |
none |
<node-3> |
<ip> |
az-c | dedicated (NoSchedule) |
none |
If your storage layer (Longhorn, local-path, etc.) only runs on one node, every stateful workload pinned to that storage class is effectively single-instance regardless of your control-plane HA count — document that explicitly here, it changes your failure-mode assumptions everywhere else.
Deployment Model: ArgoCD GitOps (app-of-apps)
git commit → git push (your git forge) → ArgoCD auto-sync → cluster
Structure:
- One root
Application(k8s/argocd/root/) pointing at a directory of childApplicationmanifests (k8s/argocd/apps/*.yaml) - Each child Application is either:
- A remote Helm chart + a second git source (
ref: values) supplying just the values file — lets you pin an upstream chart version while keeping your values under normal git history/review - A plain git directory of raw manifests (optionally with a
kustomization.yaml)
- A remote Helm chart + a second git source (
argocd.argoproj.io/sync-waveannotations control ordering across Applications (lower number syncs first)
Never kubectl apply/patch/delete a resource ArgoCD manages, except:
- Pure cleanup of stuck/dead state (e.g. deleting a permanently-failed hook Job so the next real sync can create a fresh one) — this is not a config change, just clearing wreckage that GitOps itself won't clean up automatically (see Gotchas below)
- Genuine one-time bootstrap circular dependencies (e.g. Vault
operator init/unseal — nothing can configure Vault's own unseal keys before Vault has generated them)
Hard Rules (adapt freely, but keep something like these)
🔴 Identify and document your storage/stateful-singleton node explicitly. Whatever node holds your CSI driver's data (Longhorn, local-path, etc.), renaming or wiping it orphans every PVC pinned there. Name it here, in caps, so nobody "cleans up" it by accident.
🔴 If you run multi-member etcd across a LAN + VPN/WireGuard overlay,
pin the advertised subnet explicitly (e.g. Talos's
cluster.etcd.advertisedSubnets). Without it, etcd may advertise on the
wrong interface and new members hang as non-promoting learners.
🔴 Run your IaC formatter (terraform fmt, etc.) before every commit that touches infra code. Wire this into CI as a hard gate, not a suggestion.
🔴 Whatever your source of truth is (Terraform, ArgoCD, both) — never manually mutate resources it manages. State drift is the single most common cause of "why did my last apply undo my manual fix" confusion. Fix the source, re-apply/re-sync, never bypass.
🔴 Never delete a PVC without confirming replica count / backup freshness first. This is always a one-way door.
🔴 Decide your commit message convention up front and enforce it. (This template's origin project uses: no co-authored-by footers, single-line commit summarizing what/why, solo-authorship assumption — adjust to your team's norms.)
🔴 Decide your git workflow (rebase vs merge) up front and stick to it cluster-wide, across every contributor/agent working in the repo.
🔴 Long-running commands should not block a synchronous session — run them in the background and poll, especially anything that waits on a Kubernetes rollout, an image pull, or a Terraform apply.
GitOps / ArgoCD Gotchas (transferable to any ArgoCD-based homelab)
🟠 A kustomization.yaml with an explicit resources: allowlist
silently drops anything you forget to list. No error, no drift shown in
ArgoCD's UI — it just reports Synced/Healthy against a manifest set that
never included your new file. Always run kubectl kustomize <dir>/
locally before pushing to confirm exactly what ArgoCD will build.
🟠 A top-level namespace: transformer in kustomization.yaml rewrites
metadata.namespace on every resource it builds — including RBAC
bindings deliberately targeting a different namespace (e.g. granting a
ServiceAccount in namespace A read access to Secrets in namespace B). If
any manifest needs cross-namespace RBAC, either drop the transformer
(safe if every resource already sets its own explicit namespace) or give
that manifest its own Application/directory.
🟠 PreSync hooks run before an Application's own normal resources are synced. A PreSync Job that depends on RBAC/ServiceAccounts defined as plain (non-hook) resources in the same Application will deadlock — it tries to start before its own permissions exist. Use PostSync instead if the hook needs resources from its own Application, or move the prerequisite RBAC into an earlier sync-wave Application.
🟠 ArgoCD hooks are not continuously reconciled by selfHeal. Once a
hook Job finishes (success, or exhausts backoffLimit), it's only
deleted+recreated during an actual new Sync operation — not by passive
drift detection, even with automated.selfHeal: true on. If you fix a
broken hook's spec and push, the Application's status.sync.revision can
show "caught up" while the live hook resource is still the old, broken
one, because no new operation actually re-ran it. To force it: delete the
stuck hook (clear argocd.argoproj.io/hook-finalizer manually if it's
stuck Terminating), and if that alone doesn't trigger a fresh full sync,
delete + re-kubectl apply -f the Application object itself.
🟠 If you route ArgoCD's own repoURL through an ingress/reverse-proxy
hostname that only listens on 80/443, don't use a non-standard port in the
URL — it'll silently time out trying to reach a port the proxy never
opened, and depending on your setup this can block every Application's
sync simultaneously (repo-server can't fetch git refs for anything).
🟠 Don't pin exact version tags for images from registries that don't
guarantee tag retention (Bitnami stopped publishing versioned tags for
free-tier images in 2025 — only latest + sha256 digests remain). Verify
a tag actually exists before pinning it, or prefer minimal base images +
a stdlib-only runtime download (e.g. Python's urllib.request to fetch a
static binary) to avoid depending on any third party's tagging policy.
🟠 Non-root containers can't apk add/apt install in most default
base images — package manager directories are root-owned. Use a
world-writable scratch dir (/tmp) for anything you need to
download/install at runtime instead.
🟠 Helm does not validate unknown values.yaml keys. A typo, or a
values schema copied from the wrong chart version's docs/examples, is
silently a no-op — not an error. Before concluding "this chart doesn't
support X," clone the chart at your exact pinned version/tag and run
helm template against your real values file, then diff the rendered
output. Don't trust a chart's current main-branch example values file
if you're pinned to an older release — schemas do change between major
versions without warning in your own values file.
Service Integration Routes
New service? Pick your stack below (adjust doc paths to match your repo):
| Need | Doc | Example |
|---|---|---|
| Authentication | project-usage/authentik-oidc.md |
OAuth2 login, RBAC groups, JWT tokens |
| Async messaging | project-usage/sqs-messaging.md |
Queue consumers, fire-and-forget, DLQ |
| Object storage | project-usage/minio-s3.md |
File uploads, backups, log backend |
| CI/CD pipeline | project-usage/cicd-workflow.md |
Pipeline syntax, image push, ArgoCD sync |
| Workflows | project-usage/temporal-workflows.md |
Long-running jobs, retries, state machines |
| Database | project-usage/database-postgres.md |
CloudNativePG setup, schema migrations, replicas |
| Monitoring | project-usage/monitoring-metrics.md |
Prometheus scrape, Grafana dashboard, alerts |
| Secrets | project-usage/vault-secrets.md |
Store credentials, rotate tokens, seal/unseal |
| Networking | project-usage/networking-ingress.md |
Public HTTPS, hostname routing, TLS |
Cluster Essentials (fill in your own inventory)
Architecture principles (adjust to taste, but these travel well):
- Immutable OS (Talos, or similar — no SSH, fully declarative config)
- Secrets in a proper secrets backend (Vault) + SOPS-encrypted manifests in
git (
*.enc.yaml, age-encrypted); never commit plaintext secrets or.env - ArgoCD app-of-apps as the single CD source of truth; two-phase bootstrap documented separately (chicken-and-egg: ArgoCD needs to exist before it can deploy itself declaratively — document your exact bootstrap steps)
- Pull-based GitOps — no kubeconfig/cluster credentials ever touch your CI runner; the runner only needs push access to git, ArgoCD does the rest
- Federated OIDC (one identity provider fronting every service that supports it)
Deployment Checklist (per new service)
- Prometheus
/metricsendpoint or ServiceMonitor, if it exposes metrics - All credentials in your secrets backend (never in plain values.yaml, pod env directly, or committed anywhere in cleartext)
- Ingress rule with TLS, if externally reachable
- Dashboard + alert rules, if metrics are exposed
- ArgoCD
Applicationmanifest added to the appropriate sync-wave file, not a standalonehelm install/kubectl applyrun by hand - Validated locally before push:
kubectl apply --dry-run=client -f,kubectl kustomize <dir>/(if applicable), orhelm templateagainst the exact pinned chart version (if Helm-sourced) - After push: confirmed ArgoCD's
status.sync.revisionactually matches your new commit — not just thatstatus.sync.statussaysSynced(see Gotchas — a stale hook can hide behind an otherwise-current app)
Git & Release
Incremental commits (service-layer grouped) tend to age well:
- Foundation & Docs
- Core Infra (CNI, ingress, cert management, storage)
- Observability Stack
- IAM & Secrets
- CI/CD & GitOps
- Messaging / Data Infrastructure
- Applications & Utilities
Grouping by layer (rather than by day or by "misc fixes") makes it much
easier to git log --oneline -- <path> your way back to why a given
piece of config looks the way it does, months later.