[ ['label' => 'lab.sh', 'path' => 'lab.sh'], ['label' => 'cluster/main.tf', 'path' => 'bootstrap/cluster/main.tf'], ['label' => 'provisioning/main.tf', 'path' => 'bootstrap/provisioning/main.tf'], ], 'blog_stack_2' => [ ['label' => 'cluster/main.tf', 'path' => 'bootstrap/cluster/main.tf'], ['label' => 'provisioning/main.tf', 'path' => 'bootstrap/provisioning/main.tf'], ], 'blog_stack_3' => [ ['label' => 'lab.sh', 'path' => 'lab.sh'], ['label' => 'cluster', 'path' => 'bootstrap/cluster/main.tf'], ['label' => 'platform', 'path' => 'bootstrap/platform/main.tf'], ['label' => 'apps', 'path' => 'bootstrap/apps/main.tf'], ['label' => 'provisioning', 'path' => 'bootstrap/provisioning/main.tf'], ['label' => 'edge', 'path' => 'bootstrap/edge/main.tf'], ], 'blog_stack_4' => [ ['label' => 'platform/main.tf', 'path' => 'bootstrap/platform/main.tf'], ], 'blog_stack_5' => [ ['label' => 'apps/main.tf', 'path' => 'bootstrap/apps/main.tf'], ], 'blog_stack_6' => [ ['label' => 'edge/main.tf', 'path' => 'bootstrap/edge/main.tf'], ['label' => 'nginx template', 'path' => 'bootstrap/edge/templates/default.conf.tftpl'], ['label' => 'HAProxy template', 'path' => 'bootstrap/edge/templates/haproxy.cfg.tftpl'], ], 'blog_stack_7' => [ ['label' => 'theme script', 'path' => 'apps/website/cv-theme.js'], ['label' => 'theme toolbar', 'path' => 'apps/website/partials/theme_toolbar.php'], ['label' => 'website CSS', 'path' => 'apps/website/styles.css'], ['label' => 'demo theme script', 'path' => 'apps/demos-static/public/theme.js'], ], 'blog_stack_8' => [ ['label' => 'media-cruncher.js', 'path' => 'apps/demos-static/public/media-cruncher/media-cruncher.js'], ['label' => 'media-cruncher page', 'path' => 'apps/demos-static/public/media-cruncher/index.html'], ], 'blog_stack_9' => [ ['label' => 'demo catalog', 'path' => 'apps/demos-static/public/index.html'], ['label' => 'network-quality.js', 'path' => 'apps/demos-static/public/network-quality/network-quality.js'], ['label' => 'dev-toolbelt.js', 'path' => 'apps/demos-static/public/dev-toolbelt/dev-toolbelt.js'], ['label' => 'architecture-simulator.js', 'path' => 'apps/demos-static/public/architecture-simulator/architecture-simulator.js'], ], 'blog_stack_10' => [ ['label' => 'sentiment-sandbox.js', 'path' => 'apps/demos-static/public/sentiment-sandbox/sentiment-sandbox.js'], ['label' => 'model-drift.js', 'path' => 'apps/demos-static/public/model-drift/model-drift.js'], ['label' => 'privacy-redactor.js', 'path' => 'apps/demos-static/public/privacy-redactor/privacy-redactor.js'], ], 'blog_stack_11' => [ ['label' => 'demos Dockerfile', 'path' => 'apps/demos-static/Dockerfile'], ['label' => 'demos web-app.yaml', 'path' => 'apps/demos-static/web-app.yaml'], ['label' => 'website demos.php', 'path' => 'apps/website/demos.php'], ], 'blog_stack_12' => [ ['label' => 'provisioning/main.tf', 'path' => 'bootstrap/provisioning/main.tf'], ['label' => 'lab.sh Pimox pipeline', 'path' => 'lab.sh'], ['label' => 'provisioning README', 'path' => 'bootstrap/provisioning/README.md'], ], 'blog_stack_13' => [ ['label' => 'preseed.cfg', 'path' => 'bootstrap/provisioning/templates/preseed.cfg.tftpl'], ['label' => 'golden-node prepare', 'path' => 'bootstrap/provisioning/templates/golden-node-prepare.sh.tftpl'], ['label' => 'prepare-template', 'path' => 'bootstrap/provisioning/templates/prepare-template.sh.tftpl'], ], 'blog_stack_14' => [ ['label' => 'lab.sh worker storage guardrail', 'path' => 'lab.sh'], ['label' => 'README Pimox defaults', 'path' => 'README.md'], ], 'blog_stack_15' => [ ['label' => 'lab.sh OpenWrt pipeline', 'path' => 'lab.sh'], ['label' => 'README OpenWrt notes', 'path' => 'README.md'], ], 'blog_stack_16' => [ ['label' => 'platform/main.tf', 'path' => 'bootstrap/platform/main.tf'], ['label' => 'README monitoring', 'path' => 'README.md'], ], ]; function renderStackSourceLinks(string $stackKey, array $sourceLinks, string $sourceBase): void { if (!isset($sourceLinks[$stackKey])) { return; } echo ''; } ?> <?php echo $text['blog_title']; ?> - <?php echo $text['name']; ?>

Homelab architecture map Git push enters Gitea, Gitea Actions validates and builds app images, OpenTofu manages cluster and provisioning layers, Debian keeps the control plane and PXE services, Pimox app workers run Argo CD, Kyverno, and app workloads on NVMe-backed VMs, OpenWrt can run as an opt-in firewall VM, and the OCI edge routes traffic into Kubernetes services. Source, validation, and images Control plane and provisioning Workers, edge, and workloads Developer laptop edit, test, push main Gitea repository https://lab2025.duckdns.org/git/ main is the release branch Gitea Actions runner Debian hosted runner runs validated deploys Validation gates Gitleaks secret scan Trivy IaC and image posture Buildx image build linux/arm64 website + demos OpenTofu + lab.sh manual infra apply path apps command for CI deploys PXE + preseed service dnsmasq TFTP, nginx HTTP Debian 13 arm64 netboot golden-node prep scripts kubeadm control plane API server and control loops workloads pushed to app workers GitOps mirror validated commit copied locally Argo CD reads deploy state GitOps + policy controllers Argo CD and Kyverno pinned to app workers Monitoring stack Prometheus, Grafana, Loki Promtail, node-exporter, KSM Storage and backups OpenEBS retained PVs Gitea dumps and monitoring data OCI edge host nginx, HAProxy, Varnish, Squid TLS, routing, caching public DNS entry point Tailscale + edge routes 30080 website, 30081 demos 3000 Gitea on Raspberry Pi Raspberry Pi 192.168.100.89 external Gitea Docker service optional edge-app worker repo home and backup source Orange Pi 5 Plus Pimox pimox-worker app nodes workers on nvme_thin_pool Argo CD, Kyverno, apps idempotent qm automation OpenWrt firewall VM VM 9050, opt-in only vmbr0 WAN, vmbr1 LAN simple firewall path DHCP optional, VLANs later Local registry :30500 php-website and demos-static pulled by app workers push workflow scan build manual infra validated Git serve boot join path Pimox template firewall VM policy + GitOps secure tunnel service traffic image pulls

    $activityKey): ?>