From 865a98ed97ccdff0232e85ae1b07b3185fd3eea6 Mon Sep 17 00:00:00 2001 From: Lumpiasty Date: Mon, 2 Mar 2026 04:03:29 +0100 Subject: [PATCH] revamp readme --- README.md | 289 +++++++++++++++++++++++----------- devenv.nix | 1 + docs/assets/cert-manager.svg | 211 +++++++++++++++++++++++++ docs/assets/cloudnativepg.svg | 22 +++ docs/assets/frigate.svg | 3 + docs/assets/gitea.svg | 1 + docs/assets/immich.svg | 29 ++++ docs/assets/llama-cpp.svg | 87 ++++++++++ docs/assets/nginx.svg | 2 + docs/assets/open-webui.png | Bin 0 -> 21666 bytes docs/assets/openbao.svg | 8 + docs/assets/renovate.svg | 1 + docs/assets/teamspeak.svg | 24 +++ 13 files changed, 582 insertions(+), 96 deletions(-) create mode 100644 docs/assets/cert-manager.svg create mode 100644 docs/assets/cloudnativepg.svg create mode 100644 docs/assets/frigate.svg create mode 100644 docs/assets/gitea.svg create mode 100644 docs/assets/immich.svg create mode 100644 docs/assets/llama-cpp.svg create mode 100644 docs/assets/nginx.svg create mode 100644 docs/assets/open-webui.png create mode 100644 docs/assets/openbao.svg create mode 100644 docs/assets/renovate.svg create mode 100644 docs/assets/teamspeak.svg diff --git a/README.md b/README.md index fa047e8..6073881 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,203 @@ # Homelab -## Goals +This repo contains configuration and documentation for my homelab setup, which is based on Talos OS for Kubernetes cluster and MikroTik router. -Wanting to set up homelab kubernetes cluster. +## Architecture -### Software +Physical setup consists of MikroTik router which connects to the internet and serves as a gateway for the cluster and other devices in the home network as shown in the diagram below. -1. Running applications - 1. NAS, backups, security recorder - 2. Online presence, website, email, communicators (ts3, matrix?) - 3. Git server, container registry - 4. Environment to deploy my own apps - 5. Some LLM server, apps for my own use - 6. Public services like Tor, mirrors of linux distros etc. - 7. [Some frontends](https://libredirect.github.io/) - 8. [Awesome-Selfhosted](https://github.com/awesome-selfhosted/awesome-selfhosted), [Awesome Sysadmin](https://github.com/awesome-foss/awesome-sysadmin) -2. Managing them hopefully using GitOps - 1. FluxCD, Argo etc. - 2. State of cluster in git, all apps version pinned - 3. Some bot to inform about updates? -3. It's a home**lab** - 1. Should be open to experimenting - 2. Avoiding vendor lock-in, changing my mind shouldn't block me for too long - 3. Backups of important data in easy to access format - 4. Expecting downtime, no critical workloads - 5. Trying to keep it reasonably up anyways +```mermaid +%%{init: {"flowchart": {"ranker": "tight-tree"}}}%% +flowchart TD -### Infrastructure + subgraph internet[Internet] + ipv4[IPv4 Internet] + ipv6[IPv6 Internet] + he_tunnel[Hurricane Electric IPv6 Tunnel Broker] + isp[ISP] + end -1. Using commodity hardware -2. Reasonably scalable -3. Preferably mobile workloads, software should be a bit more flexible than me moving disks and data -4. Replication is overkill for most data -5. Preferably dynamically configured network - 1. BGP with OpenWRT router - 2. Dynamically allocated host subnets - 3. Load-balancing (MetalLB?), ECMP on router - 4. Static IP configurations on nodes -6. IPv6 native, IPv4 accessible - 1. IPv6 has whole block routed to us which gives us control over address routing and usage - 2. Which allows us to expose services directly to the internet without complex router config - 3. Which allows us to use eg. ExternalDNS to autoconfigure domain names for LB - 4. But majority of the world still runs IPv4, which should be supported for public services - 5. Exposing IPv4 service may require additional reconfiguration of router, port forwarding, manual domain setting or controller doing this some day in future - 6. One public IPv4 address means probably extensive use of rule-based ingress controllers - 7. IPv6 internet from pods should not be NATed - 8. IPv4 internet from pods should be NATed by router - -### Current implementation idea - -1. Cluster server nodes running Talos -2. OpenWRT router - 1. VLAN / virtual interface, for cluster - 2. Configuring using Ansible - 3. Peering with cluster using BGP - 4. Load-balancing using ECMP -3. Cluster networking - 1. Cilium CNI - 2. Native routing, no encapsulation or overlay - 3. Using Cilium's network policies for firewall needs - 4. IPv6 address pool - 1. Nodes: 2001:470:61a3:100::/64 - 2. Pods: 2001:470:61a3:200::/64 - 3. Services: 2001:470:61a3:300::/112 - 4. Load balancer: 2001:470:61a3:400::/112 - 5. IPv4 address pool - 1. Nodes: 192.168.1.32/27 - 2. Pods: 10.42.0.0/16 - 3. Services: 10.43.0.0/16 - 4. Load balancer: 10.44.0.0/16 -4. Storage - 1. OS is installed on dedicated disk - 2. Mayastor managing all data disks - 1. DiskPool for each data disk in cluster, labelled by type SSD or HDD - 2. Creating StorageClass for each topology need (type, whether to replicate, on which node etc.) - -## Working with repo - -Repo is preconfigured to use with nix and vscode - -Install nix, vscode should pick up settings and launch terminals in `nix develop` with all needed utils. - -## Bootstrapping cluster - -1. Configure OpenWRT, create dedicated interface for connecting server - 1. Set up node subnet, routing - 2. Create static host entry `kube-api.homelab.lumpiasty.xyz` pointing at ipv6 of first node -2. Connect server -3. Grab Talos ISO, dd it to usb stick -4. Boot it and using keyboard set up static ip ipv6 subnet, should become reachable from pc -5. `talosctl gen config homelab https://kube-api.homelab.lumpiasty.xyz:6443` -6. Generate secrets `talosctl gen secrets`, **backup, keep `secrets.yml` safe** -7. Generate config files `make gen-talos-config` -8. Apply config to first node `talosctl apply-config --insecure -n 2001:470:61a3:100::2 -f controlplane.yml` -9. Wait for reboot then `talosctl bootstrap --talosconfig=talosconfig -n 2001:470:61a3:100::2` -10. Set up router and CNI - -## Updating Talos config - -Update patches and re-generate and apply configs. + subgraph home[Home network] + router[MikroTik Router] + cluster[Talos cluster] + lan[LAN] + mgmt[Management network] + cam[Camera system] + router --> lan + router --> cluster + router --> mgmt + router --> cam + end + ipv4 -- "Public IPv4 address" --> isp + ipv6 -- "Routed /48 IPv6 prefix" --> he_tunnel -- "6in4 Tunnel" --> isp + isp --> router ``` -make gen-talos-config -make apply-talos-config + +Devices are separated into VLANs and subnets for isolation and firewalling between devices and services. Whole internal network is configured to eliminate NAT where unnecessary. Pods on the Kubernetes cluster communicate with the router using native IP routing, there is no encapsulation, overlay network nor NAT on the nodes. Router knows where to direct packets destined for the pods because the cluster announces its IP prefixes to the router using BGP. Router also performs NAT for IPv4 traffic from the cluster to and from the internet, while IPv6 traffic is routed directly to the internet without NAT. High level logical routing diagram is shown below. + +```mermaid +flowchart TD + isp[ISP] --- gpon + + subgraph device[MikroTik CRS418-8P-8G-2s+] + direction TB + gpon[SFP GPON ONU] + pppoe[PPPoE client] + + he_tunnel[HE Tunnel] + + router[Router]@{ shape: cyl } + + dockers[""" + Dockers Containers (bridge) + 2001:470:61a3:500::/64 + 172.17.0.0/16 + """]@{ shape: cloud } + tailscale["Tailscale Container"] + + lan[""" + LAN (vlan2) + 2001:470:61a3::/64 + 192.168.0.0/24 + """]@{ shape: cloud } + + mgmt[""" + Management network (vlan1) + 192.168.255.0/24 + """]@{ shape: cloud } + + cam[""" + Camera system (vlan3) + 192.168.3.0/24 + """]@{ shape: cloud } + + cluster[""" + Kubernetes cluster (vlan4) + 2001:470:61a3:100::/64 + 192.168.1.0/24 + """]@{ shape: cloud } + + gpon --- pppoe -- """ + 139.28.40.212 + Default IPv4 gateway + """ --- router + + pppoe --- he_tunnel -- """ + 2001:470:61a3:: incoming + Default IPv6 gateway + """ --- router + + router -- """ + 2001:470:61a3:500:ffff:ffff:ffff:ffff + 172.17.0.1/16 + """ --- dockers --- tailscale + + router -- """ + 2001:470:61a3:0:ffff:ffff:ffff:ffff + 192.168.0.1 + """--- lan + + router -- """ + 192.168.255.10 + """--- mgmt + + router -- "192.168.3.1" --- cam + router -- """ + 2001:470:61a3:100::1 + 192.168.1.1 + """ --- cluster + + end + + subgraph k8s[K8s cluster] + direction TB + pod_network[""" + Pod networks + 2001:470:61a3:200::/104 + 10.42.0.0/16 + (Dynamically allocated /120 IPv6 and /24 IPv4 prefixes per node) + """]@{ shape: cloud } + + service_network[""" + Service network + 2001:470:61a3:300::/112 + 10.43.0.0/16 + (Advertises vIP addresses via BGP from nodes hosting endpoints) + """]@{ shape: cloud } + + load_balancer[""" + Load balancer network + 2001:470:61a3:400::/112 + 10.44.0.0/16 + (Advertises vIP addresses via BGP from nodes hosting endpoints) + """]@{ shape: cloud } + end + + cluster -- "Routes exported via BGP" ----- k8s ``` + +Currently the k8s cluster consists of single node (hostname anapistula-delrosalae), which is a PC with Ryzen 5 3600, 64GB RAM, RX 580 8GB (for accelerating LLMs), 1TB NVMe SSD, 2TB and 3TB HDDs and serves both as control plane and worker node. + +## Software stack + +The cluster itself is based on [Talos Linux](https://www.talos.dev/) (which is also a Kubernetes distribution) and uses [Cilium](https://cilium.io/) as CNI, IPAM, kube-proxy replacement, Load Balancer, and BGP control plane. Persistent volumes are managed by [OpenEBS LVM LocalPV](https://openebs.io/docs/user-guides/local-storage-user-guide/local-pv-lvm/lvm-overview). Applications are deployed using GitOps (this repo) and reconciled on cluster using [Flux](https://fluxcd.io/). Git repository is hosted on [Gitea](https://gitea.io/) running on a cluster itself. Secets are kept in [OpenBao](https://openbao.org/) (HashiCorp Vault fork) running on a cluster and synced to cluster objects using [Vault Secrets Operator](https://github.com/hashicorp/vault-secrets-operator). Deployments are kept up to date using self hosted [Renovate](https://www.mend.io/renovate/) bot updating manifests in the Git repository. Incoming HTTP traffic is routed to cluster using [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/) and certificates are issued by [cert-manager](https://cert-manager.io/) with [Let's Encrypt](https://letsencrypt.org/) ACME issuer with [cert-manager-webhook-ovh](https://github.com/aureq/cert-manager-webhook-ovh) resolving DNS-01 challanges. Cluster also runs [CloudNativePG](https://cloudnative-pg.io/) operator for managing PostgreSQL databases. High level core cluster software architecture is shown on the diagram below. + +```mermaid +flowchart TD + router[MikroTik Router] + router -- "Routes HTTP traffic" --> nginx + cilium -- "Announces routes via BGP" --> router + subgraph cluster[K8s cluster] + direction TB + flux[Flux CD] -- "Reconciles manifests" --> kubeapi[Kube API Server] + flux -- "Fetches Git repo" --> gitea[Gitea] + + + kubeapi -- "Configs, Services, Pods" --> cilium[Cilium] + cilium -- "Routing" --> services[Services] -- "Endpoints" --> pods[Pods] + cilium -- "Configures routing, interfaces, IPAM" --> pods[Pods] + + + kubeapi -- "Ingress rules" --> nginx[NGINX Ingress Controller] -- "Routes HTTP traffic" ---> pods + + kubeapi -- "Certificate requests" --> cert_manager[cert-manager] -- "Provides certificates" --> nginx + cert_manager -- "ACME DNS-01 challanges" --> dns_webhook[cert-manager-webhook-ovh] -- "Resolves DNS challanges" --> ovh[OVH DNS] + cert_manager -- "Requests DNS-01 challanges" --> acme[Let's Encrypt ACME server] -- "Verifies domain ownership" --> ovh + + kubeapi -- "Assigns pods" --> kubelet[Kubelet] -- "Manages" --> pods + + kubeapi -- "PVs, LvmVols" --> openebs[OpenEBS LVM LocalPV] + openebs -- "Mounts volumes" --> pods + openebs -- "Manages" --> lv[LVM LVs] + + kubeapi -- "Gets Secret refs" --> vault_operator[Vault Secrets Operator] -- "Syncs secrets" --> kubeapi + vault_operator -- "Retrieves secrets" --> vault[OpenBao] -- "Secret storage" --> lv + vault -- "Auth method" --> kubeapi + + gitea -- "Stores repositories" --> lv + + gitea --> renovate[Renovate Bot] -- "Updates manifests" --> gitea + + + end +``` + + + +## Applications / Services + +| Logo | Name | Address | Description | +|------|------|---------|-------------| +| Gitea | Gitea | https://gitea.lumpiasty.xyz/ | Private Git repository hosting and artifact storage (Docker, Helm charts) | +| OpenBao | OpenBao | https://openbao.lumpiasty.xyz:8200/ | Secret storage (HashiCorp Vault compatible) | +| Renovate | Renovate | | Bot for keeping dependencies up to date | +| cert-manager | cert-manager | | Automatic TLS certificate management | +| Nginx | Nginx Ingress Controller | | Ingress controller for routing external traffic to services in the cluster | +| CloudNativePG | CloudNativePG | | PostgreSQL operator for managing PostgreSQL instances | +| Immich | Immich | https://immich.lumpiasty.xyz/ | Self-hosted photo and video backup and streaming service | +| iSpeak3 | iSpeak3.pl | [ts3server://ispeak3.pl](ts3server://ispeak3.pl) | Public TeamSpeak 3 voice communication server | +| LLaMA.cpp | LLaMA.cpp | https://llama.lumpiasty.xyz/ | LLM inference server running local models with GPU acceleration | +| Open WebUI | Open WebUI | https://openwebui.lumpiasty.xyz/ | Web UI for chatting with LLMs running on the cluster | +| Frigate | Frigate | https://frigate.lumpiasty.xyz/ | NVR for camera system with AI object detection and classification | + diff --git a/devenv.nix b/devenv.nix index 21d7479..fa779c7 100644 --- a/devenv.nix +++ b/devenv.nix @@ -40,6 +40,7 @@ in restic openbao pv-migrate + mermaid-cli ]; # Scripts diff --git a/docs/assets/cert-manager.svg b/docs/assets/cert-manager.svg new file mode 100644 index 0000000..31646eb --- /dev/null +++ b/docs/assets/cert-manager.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/cloudnativepg.svg b/docs/assets/cloudnativepg.svg new file mode 100644 index 0000000..0d48a57 --- /dev/null +++ b/docs/assets/cloudnativepg.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/frigate.svg b/docs/assets/frigate.svg new file mode 100644 index 0000000..3d01f2a --- /dev/null +++ b/docs/assets/frigate.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/gitea.svg b/docs/assets/gitea.svg new file mode 100644 index 0000000..4329134 --- /dev/null +++ b/docs/assets/gitea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/immich.svg b/docs/assets/immich.svg new file mode 100644 index 0000000..376fa6f --- /dev/null +++ b/docs/assets/immich.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/docs/assets/llama-cpp.svg b/docs/assets/llama-cpp.svg new file mode 100644 index 0000000..dcbe9cc --- /dev/null +++ b/docs/assets/llama-cpp.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/nginx.svg b/docs/assets/nginx.svg new file mode 100644 index 0000000..27062a8 --- /dev/null +++ b/docs/assets/nginx.svg @@ -0,0 +1,2 @@ + +file_type_nginx \ No newline at end of file diff --git a/docs/assets/open-webui.png b/docs/assets/open-webui.png new file mode 100644 index 0000000000000000000000000000000000000000..10c84f440ced21353ee824440758cbd080c7bf55 GIT binary patch literal 21666 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 literal 0 HcmV?d00001 diff --git a/docs/assets/openbao.svg b/docs/assets/openbao.svg new file mode 100644 index 0000000..5187590 --- /dev/null +++ b/docs/assets/openbao.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/assets/renovate.svg b/docs/assets/renovate.svg new file mode 100644 index 0000000..c45aa45 --- /dev/null +++ b/docs/assets/renovate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/teamspeak.svg b/docs/assets/teamspeak.svg new file mode 100644 index 0000000..351cedf --- /dev/null +++ b/docs/assets/teamspeak.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file