repoServer CPU limit (500m) was too tight once the SOPS sidecar added real decrypt work under the liveness probe's 1s timeout — repo-server kept getting killed mid-sync. Raised to 1000m (node has 23+ idle cores, no scarcity). Separately, the generate script's doc-separator fix exposed that several .enc.yaml files (cloudflared, temporal, authentik, loki) are raw Helm-values snippets, not K8s manifests — ArgoCD hard-failed the whole batch on the first one missing 'kind:'. Script now skips those, so correctly-shaped Secrets (grafana-admin included) sync independently. grafana-admin also needed an admin-user key alongside admin-password — the chart looks up both from the same existingSecret.
Homelab Bootstrap — Single-Cluster, GitOps-Ready
Run once manually, GitOps forever after.
This bootstrap breaks the ArgoCD ↔ Forgejo circular dependency by:
- Installing infrastructure in correct dependency order
- Pointing ArgoCD at a GitHub mirror initially
- Cutting over to Forgejo once healthy
- Using Helm for reproducible installs
- Ensuring ArgoCD adopts (not duplicates) bootstrap resources
Prerequisites
- Talos cluster running (terraform applied)
- kubectl configured (
KUBECONFIGpoints at cluster) - Helm 3 installed
- SOPS age key at
~/.sops/homelab-age.key - GitHub mirror of this repo (for initial ArgoCD source)
Directory Structure
bootstrap/
├── phase1-storage/ # Longhorn via Helm
├── phase2-cnpg/ # CNPG operator via Helm
├── phase3-forgejo/ # Forgejo DB + Forgejo via Helm
├── phase4-argocd/ # ArgoCD via Helm → GitHub initially
└── phase5-cutover/ # Switch ArgoCD source to Forgejo
Usage
# From repo root:
./bootstrap.sh
# Or step-by-step:
./bootstrap.sh phase1 # Storage
./bootstrap.sh phase2 # CNPG
./bootstrap.sh phase3 # Forgejo
./bootstrap.sh phase4 # ArgoCD (GitHub mirror)
./bootstrap.sh phase5 # Cut over to Forgejo
Design Principles
- DRY: Helm values used by both bootstrap and ArgoCD
- Single Source of Truth: Manifests match what ArgoCD will manage
- Idempotent: Can re-run phases safely
- Adoption Ready: Resources have
argocd.argoproj.io/sync-options: Prune=false - Dependency Ordered: Each phase waits for previous to be Ready
Phase Details
Phase 1: Storage (Longhorn)
Installs Longhorn with:
- 3-node HA configuration
- Unified
longhornStorageClass (default) - Special
longhorn-cnpgStorageClass with postgres UID/GID mount options - CSI plugin tolerations for control-plane nodes
Source of Truth: phase1-storage/longhorn-values.yaml
Phase 2: CNPG Operator
Installs CloudNativePG operator with:
- CRD registration (blocks until CRD available)
- Webhook configuration
- Monitoring enabled
Source of Truth: phase2-cnpg/cnpg-values.yaml
Phase 3: Forgejo Database + Forgejo
- Creates
forgejo-dbCNPG Cluster - Waits for cluster Ready (PostgreSQL accepting connections)
- Installs Forgejo via Helm pointing at
forgejo-db-rwservice - Waits for Forgejo healthy
Source of Truth:
phase3-forgejo/forgejo-db.yaml(CNPG Cluster CR)phase3-forgejo/forgejo-values.yaml(Helm values)
Phase 4: ArgoCD (GitHub Mirror)
Installs ArgoCD via Helm, then applies root app-of-apps pointing at GitHub mirror.
This is the circle-breaker: ArgoCD syncs from GitHub (not Forgejo) initially.
Source of Truth:
phase4-argocd/argocd-values.yamlphase4-argocd/root-app-github.yaml(repoURL = GitHub)
ArgoCD adopts Phases 1-3 resources (no duplication) because manifests match.
Phase 5: Cut Over to Forgejo
- Push repo to Forgejo
- Update root app
repoURLfrom GitHub → Forgejo - ArgoCD re-syncs from Forgejo
The circle is broken. GitHub mirror is now disaster recovery only.
Post-Bootstrap
All changes via Git:
git commit -m "feat(app): add new service"
git push forgejo main
# ArgoCD auto-syncs
Troubleshooting
- Phase stuck? Check
kubectl get events -n <namespace> --sort-by='.lastTimestamp' - ArgoCD duplicating? Verify manifests match exactly (Helm values ↔ ArgoCD Application)
- Forgejo won't start? Check CNPG cluster Ready:
kubectl get cluster forgejo-db -n cicd - Can't push to Forgejo? Verify ingress-nginx healthy, DNS resolves
forgejo.riotpiao.com
Migration from Old Bootstrap
If you have existing k8s/bootstrap-local/:
- DO NOT delete existing resources (Longhorn data!)
- Run refined bootstrap in "adoption mode" (no delete, just apply)
- Verify ArgoCD shows "Synced" for all apps
- Archive old bootstrap:
git mv k8s/bootstrap-local k8s/archive/bootstrap-local-v1