FluxCD GitOps configuration for my homelab Kubernetes cluster.
- Hardware
- List of applications
- Repository structure
- Structure idea
- Update strategy
- Secrets management
- Local testing
I run all virtual machines inside a single physical machine with Proxmox VE:
- CPU:
Intel(R) Xeon(R) CPU E5-2670 v3 - RAM:
64 GB - SSD:
1.5 TB
Three VMs on Proxmox VE running Talos Linux as the Kubernetes node OS.
- cilium — eBPF-based CNI, load balancer, and network policy engine
- envoy-gateway — Kubernetes Gateway API implementation based on Envoy
- cert-manager — X.509 certificate management
- external-dns — Synchronizes Kubernetes Services and Ingresses with DNS providers
- istio — Service mesh for microservices
- external-secrets — External Secrets Operator pulls runtime credentials from Vaultwarden into Kubernetes Secrets
- vaultwarden — Bitwarden-compatible server used as the secret store
- linstor — Software-defined storage with DRBD replication (CSI
localandreplicatedclasses) - cloudnative-pg — PostgreSQL operator
- csi-nfs — CSI NFS driver for RWX volumes
- victoria-metrics — VictoriaMetrics k8s stack with Grafana for metrics and alerting
- victoria-logs — VictoriaLogs for log aggregation
- blackbox-exporter — Blackbox Exporter for HTTP/TCP/ICMP endpoint probing
- metrics-server — Kubernetes resource metrics for HPA/VPA
- arc-controller — GitHub Actions Runner Controller for self-hosted runners
- arc-runners — GitHub Actions Runner Scale Set for autoscaling CI runners
- immich — Self-hosted photo management
- jellyfin — Media server
- n8n — Workflow and AI automation
- httpbin — HTTP request and response test service
- qbittorrent — BitTorrent client
- it-tools — Collection of useful IT utilities
- podinfo — Test workload for validating deployments
.
├── apps
│ ├── base # Reusable app definitions (HelmRelease, HelmRepository, namespace, …)
│ │ ├── cert-manager
│ │ ├── cilium
│ │ ├── envoy-gateway
│ │ └── …
│ └── bundles # Environment aggregations with version-constraint patches
│ ├── dev-flex # Loose constraints — picks up any new release
│ └── prod-stable # Exact pins — only explicitly approved versions
├── clusters
│ ├── local # Local Kind cluster for development and testing
│ │ └── …
│ └── homelab # Production Talos cluster
│ ├── apps
│ │ ├── kustomization.yaml # Includes prod-stable bundle + cluster overlays + patches
│ │ └── …
│ ├── bundle.yaml # Flux Kustomization → ./clusters/homelab/apps
│ └── crds.yaml # Flux Kustomization → ./crds/homelab
├── crds # Operator CRDs reconciled before app bundles
│ └── homelab
├── scripts # Utility scripts (validation, SOPS helpers)
└── .github/workflows
├── e2e.yaml # End-to-end test on a Kind cluster
├── commitlint.yaml # Conventional commit message validation
└── test.yaml # Manifest validation and linting
The basic idea is to define three levels of Kustomize layering:
- Base (
apps/base/<name>) — common HelmRelease defaults, HelmRepository, and namespace. No environment specifics. - Bundle (
apps/bundles/<bundle>) — aggregates base apps and overrides onlyspec.chart.spec.versionvia a single patch file. - Cluster (
clusters/<cluster>/apps/) — entry point that includes a bundle plus cluster-specific resources (storage, routing, secrets refs, network policies) and values patches.
Application updates are managed by Renovate tracking Docker container image tags. Each cluster values file that overrides an application image includes a # renovate: datasource=docker annotation. Renovate detects new image tags and opens PRs.
Update flow:
Renovate detects new image tag → Opens PR with image bump → CI validates manifests → Review & merge → Flux reconciles homelab
Helm chart versions remain pinned in prod-stable and are updated manually when needed. Renovate does not track chart version changes.
- local — secrets are encrypted in Git with SOPS (age keys) and decrypted by Flux at reconcile time using the
sops-agesecret influx-system. - homelab (production) — External Secrets Operator pulls runtime credentials from Vaultwarden (via the in-cluster bitwarden-cli webhook) into Kubernetes Secrets. SOPS is only used to solve the chicken-and-egg problem during initial cluster bootstrap.
Requirements:
flux,kubectl, andkindavailable in PATHclusters/local/sops.agekeypresent formake bootstrapGITHUB_OWNERandGITHUB_REPOexported in the environment
make bootstrap # create Kind cluster and bootstrap Flux
make reconcile # trigger Flux reconciliation
make wait # wait for all Kustomizations and HelmReleases to become Ready
make smoke # run basic health checks (podinfo HTTP probe)
make e2e # full end-to-end: bootstrap → reconcile → wait → smoke
make validate # run manifest validation (kustomize build + kubeconform)
make clean # delete the local Kind cluster