Using GitOps principles and workflow to manage a lightweight k3s cluster.
I've used Techno Tim's k3s-ansible playbook to deploy a 4 node cluster: one control-plane node and three workers across one Proxmox host plus two bare-metal Kubernetes nodes.
| Hostname | Model | CPU | RAM | Storage | Proxmox |
|---|---|---|---|---|---|
| wilshere | Beelink EQ14 | Intel N150 | 16GB | 500GB NVMe | 9.2.3 |
| Node | Role | Host type | RAM | IP |
|---|---|---|---|---|
| arteta | control-plane | Proxmox VM | 6GB | 10.99.99.11 |
| saliba | worker | Proxmox VM | 6GB | 10.99.99.10 |
| jhapa | worker | bare metal | 16GB | 10.99.99.13 |
| lalitpur | worker | bare metal | 16GB | 10.99.99.14 |
| Description | Spec |
|---|---|
| Server | Ugreen NAS |
| IP | 10.99.99.151 |
TrueNAS Scale acts as a backup server only.
| Description | Spec |
|---|---|
| Server | SONY VAIO - SVE14126CXB (2012) |
| RAM | 8GB (maxed out) |
| CPU | Intel i5-3210M |
| SSD (os) | 128GB |
| SSD | 1TB |
| Use case | Storage type | Notes |
|---|---|---|
| Application PVCs | Longhorn | Default distributed block storage for most app data/config volumes. |
| Local/single-replica PVCs | Longhorn variants | longhorn-local and longhorn-single for selected workloads. |
| Shared RWX PVCs | SMB CSI | Used for shared volumes such as Immich libraries and Gickup backups. |
| Media/music library mounts | Direct NFS pod volumes | Used by media/music apps against Ugreen NAS 10.99.99.151. |
There is no NFS StorageClass in the cluster. NFS is still used directly in selected HelmReleases for media paths such as /volume2/media, /volume2/media/movies, /volume2/media/shows, and /volume2/media/music.
Beelink mini PCs & TrueNAS server
- Create flux namespace and the necessary sops secret
export SOPS_AGE_KEY_FILE='<path-to-key.txt>'
make bootstrap0- Flux installation
export GITHUB_TOKEN='ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
make bootstrap| Role | Provider | Notes |
|---|---|---|
Service LoadBalancer provider |
MetalLB | Allocates service external IPs from first-pool (10.99.99.20-10.99.99.30) for services such as Envoy Gateway. |
| Control plane LB provider | kube-vip | Provides the Kubernetes API/control-plane VIP at 10.99.99.222. kube-vip service load balancing is disabled (svc_enable=false). |
| Service traffic | K3s embedded kube-proxy | K3s runs kube-proxy inside the k3s server/k3s agent process, not as a Kubernetes DaemonSet. It handles ClusterIP/NodePort/service NAT using iptables mode. |
| Pod networking | Flannel VXLAN | K3s built-in flannel provides pod networking via flannel.1 and cni0. |
K3s servicelb (klipper-lb) is disabled, so it is not providing service LoadBalancer functionality.
This homelab uses a dual ExternalDNS setup to manage both internal (homelab) and external (public) DNS records automatically from Kubernetes resources.
Two ExternalDNS Instances:
-
external-dns (AdGuard Home) - Internal DNS
- Provider: AdGuard Home via webhook
- Sources: Gateway API HTTPRoute resources
- Purpose: Automatically creates A records for internal service hostnames pointing to Envoy Gateway
- Usage: Internal homelab DNS resolution
-
external-dns-cloudflare - External DNS
- Provider: Cloudflare
- Sources: DNSEndpoint CRDs only
- Purpose: Creates CNAME records pointing to Cloudflare Tunnel for external access
- Usage: Public DNS records for services accessible outside the homelab
For a service to be accessible both internally and externally:
-
Create an HTTPRoute attached to Envoy Gateway (automatically managed by AdGuard ExternalDNS)
apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: notes spec: parentRefs: - name: envoy-internal namespace: network hostnames: - notes.${HOMELAB_DOMAIN} rules: - backendRefs: - name: notes port: 80
→ AdGuard creates an internal A record for
notes.example.compointing to Envoy Gateway. -
Create a DNSEndpoint in
kubernetes/apps/network/external-dns-cloudflare/dnsendpoints.yaml(managed by Cloudflare ExternalDNS)apiVersion: externaldns.k8s.io/v1alpha1 kind: DNSEndpoint metadata: name: homelab-notes spec: endpoints: - dnsName: notes.${HOMELAB_DOMAIN} recordType: CNAME targets: - ${CF_TUNNEL_ID}.cfargotunnel.com
→ Cloudflare creates:
notes.example.com → CNAME → tunnel
Result: Same DNS name exists in both providers with different targets - internal clients use AdGuard (direct to Envoy Gateway), external clients use Cloudflare (via tunnel).
There are no Kubernetes Ingress resources in this cluster; routing is handled with Gateway API resources (Gateway, HTTPRoute, and TCPRoute) through Envoy Gateway.
This homelab implements a comprehensive three-tier backup strategy covering databases, application data, and file systems.
| Type | Primary | Cloud |
|---|---|---|
| PostgreSQL | CNPG Cluster | Cloudflare R2 |
| Volumes | Longhorn Volumes | Cloudflare R2 |
- Tool: CloudNative-PG (CNPG) operator with Point-in-Time Recovery
- Architecture: 2-replica PostgreSQL clusters for high availability
- Backup: Continuous WAL archiving and base backups to Cloudflare R2
- Recovery: Point-in-Time Recovery (PITR) capability
- Covers: Immich, Movary, Vikunja, Speedtest Tracker
- Tool: Longhorn volume snapshots and backups
- Schedule: Automated snapshots and backups
- Storage: Longhorn distributed block storage with backups to Cloudflare R2
- Covers: All persistent volumes including SQLite databases, application data, and configuration files
- Tool: Restic
- Schedule: Daily at 03:00 AM
- Storage: External HDD
- Tool: Rclone → Google Drive
- Schedule: Every 2 days at 04:30 AM
- sops (secrets management)
- age (encryption)
- precommit
- flux
- Proxmox Cloud init Template - https://technotim.live/posts/cloud-init-cloud-image/
