Skip to content

Home Lab Setup Details

Hardware

Compute

  • CPU: AMD Ryzen 5 9600X (6 cores, 12 threads, Zen 5)
  • RAM: 64 GB DDR5
  • Networking: 2.5 GbE

Storage

Three ZFS pools, each with a distinct role:

Pool Layout Raw Usable Role
tank 4× 4 TB HDD, RAIDZ1 16 TB 12 TB Primary bulk storage (default StorageClass)
backup 2× 3 TB HDD, mirror 6 TB 3 TB Backup pool for replication and snapshots
fast 1× NVMe SSD ~1.5 TB ~1.5 TB Latency-sensitive workloads (databases, metadata)

Every persistent volume in the cluster becomes a dedicated ZFS dataset — no loopback files, no shared folders on disk, nothing living on the OS disk. Compression and checksums are on by default.

Operating System

Minimal Ubuntu Server on bare metal. The only role of the host OS is to run k0s and keep the ZFS pools healthy. No desktop, no snap store, no bundled extras I don't need.

Kubernetes

  • Distribution: k0s — single-binary, single-node.
  • Previously: Proxmox + manual kubeadm. I switched to k0s because I don't need a VM between me and my cluster, and I don't need the ceremony of kubeadm for a single node.

I don't pin Kubernetes or k0s versions in this document because I run the latest stable release on both. The source of truth for what's actually running is k0s version and the HelmReleases in the fluxcd repo.

Platform Layer

Storage — OpenEBS ZFS-LocalPV

PVCs are provisioned as ZFS datasets via the OpenEBS ZFS CSI driver. Two StorageClasses:

  • tank (default) → tank/k0s pool, for general workloads
  • fastfast/k0s pool, for databases and other latency-sensitive PVCs

Because each PVC is its own dataset, I get per-workload snapshots, per-workload quotas, and I can zfs send any PVC to the backup pool or off-site without touching the workload.

Networking — Cilium

CNI with eBPF. I also use Cilium's L2 announcement feature to publish LoadBalancer IPs onto the LAN, which replaces my previous kube-vip setup — one fewer moving part.

Ingress — Traefik

All HTTP(S) traffic enters through Traefik. Automatic TLS via Let's Encrypt, security-header middleware applied globally, IngressRoute CRDs for per-service config.

Databases — CloudNativePG

Every service that needs Postgres gets its own CNPG Cluster. Backups, failover, and connection pooling are handled by the operator. No more handrolled StatefulSets.

Observability — kube-prometheus-stack + Loki + Alloy

  • Prometheus + Alertmanager for metrics and alerting
  • Loki for log storage
  • Grafana Alloy for log collection and forwarding
  • Grafana for dashboards

Auth — Authentik

Central SSO for internal services. Backed by its own CNPG Postgres cluster.

Infrastructure as Code

Everything in the cluster is declared and reconciled by FluxCD from homelab-k8s-fluxcd. Secrets are encrypted with SOPS + Age; the repo has a pre-commit hook and a CI job that blocks any attempt to push plaintext secrets.

Repository layout follows the standard Flux structure:

clusters/homelab/         # Flux entry point for this cluster
infrastructure/           # Cilium L2, Traefik, CNPG operator, storage, monitoring
apps/                     # Everything else: gitlab, matrix, immich, media, websites, ...
secrets/                  # SOPS-encrypted secrets only

Management

  • SSH for host-level access
  • kubectl / k9s for cluster access
  • Flux CLI for reconciliation status and debugging
  • Grafana for operational dashboards and alerts

Backup Strategy

  • All PVCs live on ZFS → free per-dataset snapshots
  • Application data replicated to the backup pool via zfs send/zfs receive
  • Critical services (GitLab, Immich, Matrix) have their own application-level backups on top of dataset snapshots