# A Tour of My Kubernetes Homelab GitOps Repo

4 min read
Table of Contents

One repo to rule the cluster

Everything in my Kubernetes homelab lives in a single git repo and gets deployed by Argo CD.

The hardware

Nothing fancy here — lil PCs:

  • An Intel NUC with an N100
  • An old Lenovo ThinkCentre M920q I picked up on ebay lol

Both run Proxmox, and the Kubernetes nodes are Talos Linux VMs on top.

The repo

The repo is laid out like this:

gitops/
├── root.yaml # The app-of-apps entry point
├── apps/ # Argo CD Application definitions
├── manifests/ # Kustomize dirs + Helm values per app
└── charts/ # My own local Helm charts

App of apps

The whole thing hangs off a single root Application that points at the gitops/apps directory. Every YAML file in there is another Argo CD Application, so adding something new to the cluster is just dropping a file in that folder and pushing.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: git@github.com:digitalsoba/homelab.git
targetRevision: HEAD
path: gitops/apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true

Every app runs with prune: true and selfHeal: true, so drift gets stomped out automatically. Argo CD even manages itself — there’s an argocd app pointing at a kustomize directory with patches for OIDC login, RBAC, and running the server in insecure mode behind the gateway.

The infrastructure layer

The addons app is a folder of Argo CD Applications for all the cluster plumbing:

  • MetalLB — LoadBalancer IPs on bare metal, handing out addresses from a small pool on my LAN via L2 advertisement
  • kgateway — my Gateway API implementation. One shared Gateway, and each app attaches its own ListenerSet + HTTPRoute
  • cert-manager — Let’s Encrypt certs via Cloudflare DNS01, with Gateway API and ListenerSet support enabled
  • external-dns — watches HTTPRoutes/Ingresses (including ones attached via ListenerSets) and creates the Cloudflare DNS records for me
  • external-secrets — syncs secrets from Infisical into the cluster
  • CloudNativePG — Postgres operator, so every app gets a proper database instead of a SQLite file in a PVC
  • Storagelocal-path-provisioner for node-local volumes, plus csi-driver-nfs pointed at my Synology NAS for shared storage
  • The little helpers — metrics-server, descheduler, and Reloader to bounce pods when their ConfigMaps/Secrets change

No secrets in git

Nothing sensitive lives in the repo. A ClusterSecretStore connects external-secrets to my Infisical project, and each app declares an ExternalSecret for what it needs.

The giant CRD problem

A dedicated crds app exists purely because some CRDs (kgateway’s gatewayparameters is ~738KB 😅) blow past the 262144-byte limit of the client-side last-applied-configuration annotation and break the sync. That app applies Gateway API, kgateway, and Prometheus Operator CRDs with per-resource Server-Side Apply, pinned to the same versions the addon charts deploy.

The actual apps

All this plumbing exists so I can run a handful of self-hosted apps:

  • Forgejo — private git hosting, backed by a CNPG Postgres cluster
  • Pocket ID — lightweight OIDC provider with passkey login; Argo CD and Miniflux both authenticate through it
  • Miniflux — RSS reader, deployed from my own local Helm chart
  • Homepage — dashboard with widgets, including one talking to the Argo CD API through a dedicated service account

Favorite trick: one IP for HTTPS and git-over-SSH

Forgejo needs port 22 for git pushes, but HTTPS goes through the shared kgateway Gateway. MetalLB’s allow-shared-ip annotation lets both Services claim the same LoadBalancer IP:

# On the Gateway (ports 80/443)
metallb.io/loadBalancerIPs: 192.0.2.10
metallb.io/allow-shared-ip: misoshoup-forge
# On the forgejo-ssh Service (port 22)
metallb.io/loadBalancerIPs: 192.0.2.10
metallb.io/allow-shared-ip: misoshoup-forge

Since the port sets don’t collide, git.misoshoup.com resolves to a single IP serving both the web UI and SSH. One hostname, one DNS record, zero weirdness in my git remotes.

Multi-source apps for upstream charts

Forgejo also shows off a nice Argo CD feature: multi-source Applications. The chart comes from Forgejo’s OCI registry, but the values file lives in my repo, pulled in via a $values ref:

sources:
- repoURL: code.forgejo.org/forgejo-helm
chart: forgejo
targetRevision: 17.1.1
helm:
valueFiles:
- $values/gitops/manifests/forgejo/values.yaml
- repoURL: git@github.com:digitalsoba/homelab.git
path: gitops/manifests/forgejo
targetRevision: HEAD
ref: values

The second source also carries supplemental manifests — the CNPG Cluster, the ExternalSecret for admin credentials, and the Gateway API routes.

Rolling my own chart

For Miniflux I skipped upstream charts and wrote a small one in gitops/charts/miniflux. It bundles the Deployment, a CNPG Postgres cluster (the generated connection URI is injected as DATABASE_URL), an ExternalSecret for the admin bootstrap credentials, and the ListenerSet + HTTPRoute for exposure. OIDC against Pocket ID is a few lines of values:

oidc:
enabled: true
discoveryEndpoint: https://auth.misoshoup.com
providerName: Pocket ID

Argo CD renders the chart straight from git — no chart registry needed.

Whats Next?

  • Monitoring?

More Posts