← blog · October 21, 2025 · 6 min read
Building the Ultimate Home Kubernetes Cluster
How I built a K3s cluster at home with mixed amd64 and ARM nodes, GitOps with FluxCD, distributed storage with Longhorn, MetalLB for networking, and a full monitoring stack.
$ bloat--level 3Bloatin'
I’ve been running services at home for a while now, but everything was scattered across different machines with no real orchestration. I finally decided to go all-in and build a proper Kubernetes cluster at home, and I’m really happy with how it turned out. This is the story of how I did it.
The hardware
The cluster is called Magi (yes, the Evangelion reference), and it runs on a mix of amd64 and ARM nodes:
- 2 amd64 machines with local disks. These are the workhorses, handling storage-heavy workloads and anything that needs more power.
- 4 Raspberry Pis (ARM nodes) for running smaller services and spreading the load.
This mixed-architecture setup means I had to be mindful of multi-arch container images for everything I deploy, but honestly most popular images already support both amd64 and arm64, so it hasn’t been a big deal.
K3s, lightweight Kubernetes
I went with K3s instead of full-blown Kubernetes. It’s lightweight, ships as a single binary, and is perfect for a home setup where you don’t want to deal with the complexity of kubeadm or managed control planes. It comes with a lot of batteries included: built-in Traefik ingress, CoreDNS, and local storage out of the box.
GitOps with FluxCD
This is probably the part I’m most proud of. Every single thing running on the cluster is defined in a single Git repository. I use FluxCD as my GitOps controller. It watches the repo and automatically reconciles the cluster state every minute.
The repo structure is clean and organized:
clusters/<cluster-name>/
├── flux-system/ # FluxCD itself
├── infra/ # Infrastructure components
└── apps/ # Application workloads
infra/
├── metallb/ # Load balancer
├── longhorn/ # Distributed storage
├── prometheus/ # Monitoring
├── glances/ # System stats
├── node-exporter/ # Hardware metrics
├── blackbox-exporter/ # Endpoint probing
└── flux-notifications/ # Chat alerts
apps/
├── ... # Your applications
The best part is the dependency chain. FluxCD lets you define dependsOn between kustomizations, so the cluster bootstraps itself in the right order:
MetalLB → MetalLB Config → Longhorn → Prometheus → Exporters → Apps
This means I can blow away the entire cluster and rebuild it from scratch. Just point FluxCD at the repo and walk away. Everything comes up in the correct order, with health checks at each step before moving to the next.
Secrets management with SOPS
Obviously you can’t just push secrets to a Git repo in plain text. I use SOPS with age encryption: secrets are encrypted at rest in the repository and decrypted by FluxCD at reconciliation time. Container registry credentials, database passwords, API tokens, all safely stored in the repo alongside everything else.
Chat notifications
FluxCD also sends me chat notifications for every deployment event. Every time a kustomization reconciles, a HelmRelease updates, or something fails, I get a message on my phone. It’s a small thing but it makes the whole setup feel alive. I always know what’s happening on the cluster without having to check.
MetalLB on bare metal
One of the first problems you run into with Kubernetes at home is LoadBalancer services. In the cloud, your provider hands you an external IP. At home? You get nothing. Your services just sit there in Pending state forever.
MetalLB solves this. I configured an L2 address pool with a small range of IPs reserved on my home network for LoadBalancer services. The L2 advertisement is configured to only announce from control-plane nodes, keeping things tidy.
Now when I create a service of type LoadBalancer, MetalLB assigns it a real IP on my home network. Combined with a local DNS setup using an internal domain, I can access everything by name, no more remembering IPs.
Longhorn for distributed storage
Storage in Kubernetes is always a pain, and it’s even worse on bare metal. I needed something that could provide persistent volumes with replication across nodes, and Longhorn was the answer.
Longhorn runs as a distributed block storage system across my amd64 nodes only. The Raspberry Pis don’t have the disk space or I/O performance to be useful as storage nodes. The local disks on the amd64 machines are where all the data lives, mounted at /var/lib/longhorn.
I’m running with a default replica count of 1 for most volumes. This is a home lab after all, not a production data center. For critical stuff like databases, I bump it to 3x replication for safety. Media volumes stay at 1 replica because I’d rather have the performance and I can always re-download a movie.
Longhorn also comes with a web UI, which makes it easy to monitor volume health, check space usage, and manage snapshots.
Monitoring: Prometheus, Glances, and Grafana
What’s the point of running a cluster if you can’t obsessively stare at dashboards? I set up a full monitoring stack:
Prometheus
I went with a hand-rolled Prometheus deployment instead of the kube-prometheus-stack Helm chart. It’s lighter, I understand exactly what it does, and for a home cluster I don’t need all the complexity that comes with the full operator.
Prometheus is pinned to a specific node with a 10Gi persistent volume on Longhorn and 7 days of retention. The scrape config is straightforward:
- Node Exporter for hardware and OS metrics from every node (CPU, memory, disk, temps)
- Blackbox Exporter for ICMP probes for network monitoring (I even ping IoT devices on my network to make sure they’re alive)
- Self-monitoring for both Prometheus and Blackbox Exporter
All of this feeds into Grafana dashboards where I can see everything at a glance: node temperatures, disk usage, network throughput, service uptime.
Glances
Glances runs as a DaemonSet with full host access (hostNetwork: true, hostPID: true), which means it’s deployed on every node in the cluster and gives me a web-based view of system resources per machine. Think of it as htop in your browser, for every node.
One fun quirk: I had to exclude one of my nodes from the Glances DaemonSet because the Glances container has a compatibility issue with Debian 13 on that machine. A little nodeAffinity rule takes care of that.
Node Exporter
Runs as a DaemonSet on every node (including ARM), tolerating all taints. It exposes hardware metrics on port 9100. Temperatures via hwmon and thermal_zone collectors are particularly useful for keeping an eye on the Raspberry Pis, which are known to thermal throttle.
What’s running on the cluster
The whole point of this is to actually run stuff, right? I’m running a mix of media servers, content management systems, custom web applications, monitoring dashboards, and even file shares for retro gaming (yes, the Gameboy from my GBA post gets its ROMs from the cluster 😄).
All accessible via internal DNS, all managed by FluxCD, all with persistent storage on Longhorn.
Lessons learned
Start simple. I didn’t build all of this in one weekend. I started with K3s + MetalLB + one app, and kept adding layers as I got comfortable.
GitOps makes a big difference. Having everything in one repo with FluxCD means I can review changes before they go live, roll back with a git revert, and sleep well knowing the cluster matches what’s in Git.
Mixed architectures work, but check your images. Most popular Docker images support multi-arch now, but every now and then you’ll hit one that’s amd64-only. Longhorn, for instance, only runs on my amd64 nodes.
Monitoring from day one. Don’t wait until something breaks to set up Prometheus. The Raspberry Pis in particular need temperature monitoring. I’ve caught thermal throttling issues early thanks to the Node Exporter + Grafana combo.
Longhorn is great for home labs. It’s not the fastest storage solution, but it’s easy to set up, the UI is good, and having the option to replicate critical data across nodes gives real peace of mind.
What’s next
- Grafana dashboards. I want to build more custom dashboards for the services I’m running.
- Cert-manager + external access. Exposing some services externally with proper TLS.
- Backup automation. Longhorn snapshots to an off-site NAS.
- More Raspberry Pis. Because why not?
If you’re thinking about building a home Kubernetes cluster, give it a go. K3s makes it approachable, FluxCD keeps it sane, and you learn a lot along the way.
finally built a real k8s cluster at home, no more random services scattered everywhere. its called magi (evangelion, obviously)
hardware: 2 amd64 boxes with local disks for heavy stuff + 4 raspberry pis. mixed arch so gotta check for multi-arch images but most do amd64+arm64 now, fine
k3s not full k8s. single binary, traefik + coredns + local storage built in
everything lives in one git repo, fluxcd reconciles every minute. layout:
clusters/<cluster-name>/
├── flux-system/ # FluxCD itself
├── infra/ # Infrastructure components
└── apps/ # Application workloads
infra/
├── metallb/ # Load balancer
├── longhorn/ # Distributed storage
├── prometheus/ # Monitoring
├── glances/ # System stats
├── node-exporter/ # Hardware metrics
├── blackbox-exporter/ # Endpoint probing
└── flux-notifications/ # Chat alerts
apps/
├── ... # Your applications
dependsOn between kustomizations so it bootstraps in order:
MetalLB → MetalLB Config → Longhorn → Prometheus → Exporters → Apps
can nuke the cluster and rebuild: point flux at the repo, walk away, health checks each step. secrets are sops + age encrypted in the repo (registry creds, db passwords, api tokens). flux pings my phone on every reconcile/update/failure, feels alive
metallb bc loadbalancer services sit Pending forever on bare metal. l2 pool, small range of reserved home ips, advertised from control-plane nodes only. local dns internal domain = no remembering ips
storage = longhorn, amd64 nodes only (pis lack disk + i/o). data at /var/lib/longhorn. replica 1 default, 3x for databases, media stays 1, can redownload a movie. web ui is decent
monitoring: hand rolled prometheus, not kube-prometheus-stack, lighter and i get it. pinned to a node, 10Gi longhorn vol, 7d retention. node exporter (cpu mem disk temps), blackbox icmp probes (even pings iot devices), self monitoring for both. grafana on top: temps, disk, network, uptime
glances daemonset, hostNetwork + hostPID, htop in browser per node. one node excluded via nodeAffinity, glances breaks on debian 13 🤷
node exporter daemonset everywhere incl arm, all taints, port 9100. hwmon/thermal_zone temps bc pis throttle
running media servers, cms, custom apps, dashboards, rom share for the gameboy (gba post) 😄
lessons: start simple, k3s + metallb + one app first. gitops = git revert rollback, sleep well. check images for arm (longhorn is amd64 only). monitor day one, caught pi throttling. longhorn not fast but easy, replication = peace of mind
todo: more grafana dashboards, cert-manager + external tls, longhorn snapshots to offsite nas, more pis
For years my home services were scattered across different machines with no orchestration. I finally went all-in and built a proper Kubernetes cluster at home, and I’m really happy with the result. Here’s how.
The hardware
The cluster is called Magi (yes, the Evangelion reference). It mixes two architectures:
- 2 amd64 machines with local disks: the workhorses for storage-heavy and demanding workloads.
- 4 Raspberry Pis (ARM) for smaller services and spreading the load.
Mixed architecture means checking that images are multi-arch, but most popular ones ship both amd64 and arm64, so it’s rarely a problem.
K3s and GitOps
I chose K3s over full Kubernetes: a single lightweight binary with Traefik ingress, CoreDNS, and local storage built in. Perfect for home.
The part I’m most proud of: everything on the cluster is defined in a single Git repository, reconciled every minute by FluxCD.
clusters/<cluster-name>/
├── flux-system/ # FluxCD itself
├── infra/ # Infrastructure components
└── apps/ # Application workloads
infra/
├── metallb/ # Load balancer
├── longhorn/ # Distributed storage
├── prometheus/ # Monitoring
├── glances/ # System stats
├── node-exporter/ # Hardware metrics
├── blackbox-exporter/ # Endpoint probing
└── flux-notifications/ # Chat alerts
apps/
├── ... # Your applications
FluxCD’s dependsOn between kustomizations makes the cluster bootstrap itself in order, with health checks at each step:
MetalLB → MetalLB Config → Longhorn → Prometheus → Exporters → Apps
I can wipe the entire cluster, point FluxCD at the repo, and walk away.
Secrets are handled with SOPS and age encryption: registry credentials, database passwords, and API tokens live encrypted in the repo and FluxCD decrypts them at reconciliation. FluxCD also sends chat notifications to my phone for every reconcile, update, or failure. A small thing, but I always know what the cluster is doing.
MetalLB
On bare metal, LoadBalancer services sit in Pending forever; no cloud provider hands you an IP. MetalLB fixes this with an L2 address pool of reserved IPs on my home network, advertised only from the control-plane nodes. Combined with local DNS on an internal domain, every service is reachable by name.
Longhorn
For persistent volumes with replication I use Longhorn, running on the amd64 nodes only; the Pis lack the disk space and I/O for storage duty. Data lives on the local disks at /var/lib/longhorn.
Replication is pragmatic: 1 replica by default, 3x for critical things like databases, and 1 for media because I can always re-download a movie. The web UI makes volume health, space usage, and snapshots easy to manage.
Monitoring
Prometheus is a hand-rolled deployment rather than kube-prometheus-stack: lighter, and I understand exactly what it does. It’s pinned to one node with a 10Gi Longhorn volume and 7 days of retention, scraping:
- Node Exporter for hardware and OS metrics (CPU, memory, disk, temps)
- Blackbox Exporter for ICMP probes, including IoT devices on my network
- Self-monitoring for both
Everything feeds Grafana dashboards: node temperatures, disk usage, network throughput, uptime.
Glances runs as a DaemonSet with hostNetwork: true and hostPID: true, giving me an htop-in-the-browser view of every node. One quirk: the Glances container breaks on Debian 13, so a nodeAffinity rule excludes that node.
Node Exporter runs as a DaemonSet on every node including the ARM ones, tolerating all taints, on port 9100. The hwmon and thermal_zone collectors are essential for the Raspberry Pis, which are prone to thermal throttling.
What’s running
Media servers, content management systems, custom web apps, monitoring dashboards, and file shares for retro gaming; the Gameboy from my GBA post gets its ROMs from the cluster 😄. All on internal DNS, all managed by FluxCD, all on Longhorn.
Lessons learned
Start simple. K3s + MetalLB + one app first, layers later.
GitOps pays off. Changes are reviewed before going live, rollback is a git revert, and the cluster always matches Git.
Mixed architectures work, but check your images. Occasionally something is amd64-only; Longhorn is, for me.
Monitor from day one. Node Exporter + Grafana caught Pi thermal throttling early.
Longhorn suits home labs. Not the fastest, but easy to run, good UI, and replication for critical data brings peace of mind.
What’s next
More custom Grafana dashboards, cert-manager with external TLS access, automated Longhorn snapshots to an off-site NAS, and more Raspberry Pis. Because why not?
If you’re considering a home Kubernetes cluster, go for it. K3s makes it approachable, FluxCD keeps it sane, and you learn a lot along the way.
In today’s ever-evolving self-hosting landscape, running a handful of scattered services across different machines simply doesn’t cut it anymore. For a long time, that was exactly my situation: services everywhere, no real orchestration, no single source of truth. Whether you’re managing three services or thirty, that kind of fragmentation eventually catches up with you. So I decided to go all-in and build a proper Kubernetes cluster at home, and I couldn’t be happier with how it turned out. It wasn’t just an infrastructure upgrade, it was a complete shift in how I think about my homelab. In this post, we’ll delve into exactly how I did it, step by step.
The hardware
Let’s start with the foundation. The cluster is called Magi (yes, the Evangelion reference), and it leverages a heterogeneous mix of amd64 and ARM nodes:
- 2 amd64 machines with local disks. These are the workhorses of the operation, handling storage-heavy workloads and anything that demands more computational power.
- 4 Raspberry Pis (ARM nodes) for running smaller services and distributing the load across the cluster.
It’s important to note that this mixed-architecture approach means being mindful of multi-arch container images for everything deployed. That said, the vast majority of popular images already support both amd64 and arm64 out of the box, so in practice it hasn’t been a significant hurdle.
K3s, lightweight Kubernetes
When it comes to the Kubernetes distribution itself, I opted for K3s rather than full-blown Kubernetes. This choice was driven by three key factors: it’s lightweight, it ships as a single binary, and it’s perfectly suited for a home environment where you don’t want to grapple with the complexity of kubeadm or managed control planes. Furthermore, it comes with plenty of batteries included: built-in Traefik ingress, CoreDNS, and local storage, all working seamlessly from the moment you install it. For a home setup, this combination of simplicity and capability is hard to beat.
GitOps with FluxCD
Now, let’s delve into the part I’m genuinely most proud of. Every single thing running on the cluster is defined in a single Git repository; the repo isn’t just documentation, it’s the cluster itself. I leverage FluxCD as my GitOps controller: it watches the repository and automatically reconciles the cluster state every minute, ensuring that what’s running always matches what’s committed.
The repository structure is clean, organized, and intentional:
clusters/<cluster-name>/
├── flux-system/ # FluxCD itself
├── infra/ # Infrastructure components
└── apps/ # Application workloads
infra/
├── metallb/ # Load balancer
├── longhorn/ # Distributed storage
├── prometheus/ # Monitoring
├── glances/ # System stats
├── node-exporter/ # Hardware metrics
├── blackbox-exporter/ # Endpoint probing
└── flux-notifications/ # Chat alerts
apps/
├── ... # Your applications
The best part, without a doubt, is the dependency chain. FluxCD lets you define dependsOn relationships between kustomizations, which means the cluster bootstraps itself in exactly the right order:
MetalLB → MetalLB Config → Longhorn → Prometheus → Exporters → Apps
The implications here are significant. I can blow away the entire cluster and rebuild it from scratch: simply point FluxCD at the repository and walk away. Everything comes up in the correct order, with health checks at each step before moving on to the next. It’s not just convenient, it’s genuinely liberating.
Secrets management with SOPS
Of course, it goes without saying that you can’t just push secrets to a Git repository in plain text. To address this, I leverage SOPS with age encryption: secrets are encrypted at rest in the repository and seamlessly decrypted by FluxCD at reconciliation time. Container registry credentials, database passwords, API tokens, all of them stored safely in the repo alongside everything else, versioned and auditable.
Chat notifications
Furthermore, FluxCD sends me chat notifications for every deployment event. Every time a kustomization reconciles, a HelmRelease updates, or something fails, a message lands on my phone. It’s a small touch, but it makes the entire setup feel alive and responsive. I always know exactly what’s happening on the cluster without ever having to check manually.
MetalLB on bare metal
One of the first challenges you’ll inevitably encounter in the home Kubernetes landscape is LoadBalancer services. In the cloud, your provider seamlessly hands you an external IP. At home? You get nothing. Your services simply sit there in Pending state forever, waiting for an IP that will never arrive.
This is where MetalLB comes in. I configured an L2 address pool with a small range of IPs reserved on my home network specifically for LoadBalancer services. It’s worth noting that the L2 advertisement is configured to only announce from control-plane nodes, keeping the network behavior tidy and predictable.
The result? When I create a service of type LoadBalancer, MetalLB assigns it a real IP on my home network. Combined with a local DNS setup using an internal domain, I can access everything by name. No more remembering IPs, no more friction.
Longhorn for distributed storage
Storage in Kubernetes is always a pain point, and it’s even more challenging on bare metal, where there’s no cloud provider waiting in the wings with managed disks. I needed a solution that could provide persistent volumes with robust replication across nodes, and after evaluating the options in the storage landscape, Longhorn proved to be the answer.
Longhorn runs as a distributed block storage system across my amd64 nodes only. The Raspberry Pis simply don’t have the disk space or I/O performance to be useful as storage nodes, so the local disks on the amd64 machines are where all the data lives, mounted at /var/lib/longhorn.
When it comes to replication, I take a pragmatic, tiered approach: a default replica count of 1 for most volumes. This is a home lab, after all, not a production data center. For critical workloads like databases, I bump it up to 3x replication for safety. Media volumes stay at 1 replica because I’d rather have the performance, and at the end of the day, I can always re-download a movie.
It’s also worth highlighting that Longhorn ships with a web UI, which makes it remarkably easy to monitor volume health, check space usage, and manage snapshots.
Monitoring: Prometheus, Glances, and Grafana
What’s the point of running a cluster if you can’t obsessively stare at dashboards? Observability isn’t just a nice-to-have, it’s a cornerstone of any healthy cluster, and it’s what transforms a black box into a system you can genuinely trust. To that end, I set up a full monitoring stack: Prometheus for metrics, Glances for per-node visibility, and Grafana to tie it all together.
Prometheus
I went with a hand-rolled Prometheus deployment rather than the kube-prometheus-stack Helm chart. It’s lighter, I understand exactly what it does, and for a home cluster I simply don’t need all the complexity that comes with the full operator.
Prometheus is pinned to a specific node with a 10Gi persistent volume on Longhorn and 7 days of retention. The scrape configuration is straightforward yet comprehensive:
- Node Exporter for hardware and OS metrics from every node (CPU, memory, disk, temps)
- Blackbox Exporter for ICMP probes for network monitoring (I even ping the IoT devices on my network to make sure they’re alive)
- Self-monitoring for both Prometheus and Blackbox Exporter
All of this feeds seamlessly into Grafana dashboards, where I can see everything at a glance: node temperatures, disk usage, network throughput, and service uptime.
Glances
Glances runs as a DaemonSet with full host access (hostNetwork: true, hostPID: true), meaning it’s deployed on every node in the cluster and provides a web-based view of system resources per machine. Think of it as htop in your browser, for every node, all the time.
One fun quirk worth mentioning: I had to exclude one of my nodes from the Glances DaemonSet because the Glances container has a compatibility issue with Debian 13 on that machine. A little nodeAffinity rule takes care of it elegantly.
Node Exporter
Node Exporter runs as a DaemonSet on every node (including the ARM ones), tolerating all taints and exposing hardware metrics on port 9100. The temperature data via the hwmon and thermal_zone collectors is particularly valuable for keeping a watchful eye on the Raspberry Pis, which are notoriously known to thermal throttle.
What’s running on the cluster
At the end of the day, the whole point of this endeavor is to actually run stuff, right? The cluster hosts a diverse mix of media servers, content management systems, custom web applications, monitoring dashboards, and even file shares for retro gaming (yes, the Gameboy from my GBA post gets its ROMs from the cluster 😄).
All of it accessible via internal DNS, all managed by FluxCD, all backed by persistent storage on Longhorn. A cohesive, self-healing whole.
Lessons learned
Start simple. It’s important to note that I didn’t build all of this in one weekend. I started with K3s + MetalLB + one app, and progressively added layers as I grew more comfortable with each piece.
GitOps makes a big difference. Having everything in one repository with FluxCD means I can review changes before they go live, roll back with a simple git revert, and sleep well knowing the cluster always matches what’s in Git. It’s not just a workflow, it’s peace of mind.
Mixed architectures work, but check your images. The vast majority of popular Docker images support multi-arch these days, but every now and then you’ll hit one that’s amd64-only. Longhorn, for instance, only runs on my amd64 nodes.
Monitoring from day one. Don’t wait until something breaks to set up Prometheus. The Raspberry Pis in particular demand temperature monitoring; I’ve caught thermal throttling issues early thanks to the Node Exporter + Grafana combination.
Longhorn is great for home labs. It’s not the fastest storage solution in the landscape, but it’s easy to set up, the UI is genuinely good, and having the option to replicate critical data across nodes provides real peace of mind.
What’s next
The journey doesn’t end here. On the roadmap:
- Grafana dashboards. Building more custom dashboards for the services I’m running.
- Cert-manager + external access. Exposing selected services externally with proper TLS.
- Backup automation. Longhorn snapshots shipped to an off-site NAS.
- More Raspberry Pis. Because why not?
Whether you’re a seasoned platform engineer or just dipping your toes into container orchestration, if you’re thinking about building a home Kubernetes cluster, I wholeheartedly encourage you to give it a go. K3s makes it approachable, FluxCD keeps it sane, and you’ll learn an enormous amount along the way.
🚀 In today’s fast-paced, hyper-connected world, where digital infrastructure underpins virtually every aspect of our daily lives, there comes a moment in every technologist’s journey when scattered services across random machines simply won’t cut it anymore. Have you ever looked at your homelab and thought, “there has to be a better way”? I certainly did. For far too long, my services lived fragmented lives across different machines with no real orchestration, no single source of truth, and no unified vision. Today, I’m beyond thrilled, genuinely humbled, and profoundly excited to share the story of how I went all-in and built the ultimate Kubernetes cluster at home, a true labor of love, and honestly? A complete game-changer.
Buckle up, dear reader, because we’re about to embark on quite the journey together. 🎢 Over the course of this deep dive, we’ll explore the hardware that powers it all, the magic of GitOps, the wonders of bare-metal load balancing, the art of distributed storage, and a monitoring stack that would make any SRE shed a single, proud tear. Ready? Let’s dive in! 🏊
The Hardware: A Symphony of Silicon 🖥️
Every great story needs a great cast of characters, and every great cluster needs great hardware. The cluster is called Magi (yes, the Evangelion reference, and if you know, you know 😉), and it represents a rich tapestry of computing architectures, a harmonious blend of amd64 and ARM nodes working together in beautiful unison:
- 💪 2 amd64 machines with local disks: These are the true workhorses of the operation, the unsung heroes handling storage-heavy workloads and anything that demands serious computational muscle.
- 🥧 4 Raspberry Pis (ARM nodes): These delightful little single-board wonders run smaller services and spread the load across the cluster with remarkable grace.
Now, it’s absolutely crucial to understand that this mixed-architecture approach isn’t without its considerations. I had to be mindful of multi-arch container images for everything I deploy. But here’s the wonderful news: the vast majority of popular images already support both amd64 and arm64, so in practice, it hasn’t been a big deal at all. Isn’t it amazing how far the container ecosystem has come?
K3s: Lightweight Kubernetes, Heavyweight Results ⚡
When it came time to choose a Kubernetes distribution, I stood at a crossroads that every homelab builder eventually reaches. Full-blown Kubernetes, with all its enterprise-grade glory? Or something leaner, meaner, and purpose-built for environments just like mine? I made a decision that would shape the entire trajectory of this project: I went with K3s instead of full-blown Kubernetes. And what a decision it was!
Why K3s, you ask? Great question! Let’s break it down:
- 🪶 Lightweight by design: It ships as a single binary, elegant, efficient, and effortless.
- 🏠 Perfect for home: No wrestling with the complexity of kubeadm or managed control planes.
- 🔋 Batteries included: Built-in Traefik ingress, CoreDNS, and local storage, all working seamlessly right out of the box.
It’s not just a Kubernetes distribution; it’s a philosophy of simplicity. In a world that constantly pushes us toward more complexity, more moving parts, and more things that can break at 3 AM, K3s is a breath of fresh air. It respects your time, it respects your hardware, and most importantly, it respects your sanity.
GitOps with FluxCD: The Beating Heart of the Cluster 💚
Now we arrive at the part of this journey that fills me with the most pride, the crown jewel of the entire endeavor, the beating heart of Magi itself. Are you ready for this? Every single thing running on the cluster is defined in a single Git repository. Yes, you read that correctly. Every deployment, every configuration, every last piece of the puzzle. Everything. Let me repeat that one more time for the people in the back: EVERYTHING. 🎤
How is such a feat possible, you might wonder? I leverage FluxCD as my GitOps controller, and it tirelessly watches the repository, automatically reconciling the cluster state every single minute of every single day, never sleeping, never complaining, never asking for a raise. The repository isn’t just documentation; it IS the cluster. This paradigm shift cannot be overstated: infrastructure as code isn’t just a best practice here, it’s a way of life.
The repo structure is a masterclass in clean organization:
clusters/<cluster-name>/
├── flux-system/ # FluxCD itself
├── infra/ # Infrastructure components
└── apps/ # Application workloads
infra/
├── metallb/ # Load balancer
├── longhorn/ # Distributed storage
├── prometheus/ # Monitoring
├── glances/ # System stats
├── node-exporter/ # Hardware metrics
├── blackbox-exporter/ # Endpoint probing
└── flux-notifications/ # Chat alerts
apps/
├── ... # Your applications
But wait, it gets better. So much better. The absolute best part is the dependency chain. FluxCD empowers you to define dependsOn relationships between kustomizations, which means the cluster orchestrates its own bootstrap in exactly the right order, like a perfectly choreographed ballet:
MetalLB → MetalLB Config → Longhorn → Prometheus → Exporters → Apps
Let that sink in for a moment. I can blow away the ENTIRE cluster and rebuild it from scratch. Just point FluxCD at the repo and walk away. ☕ Go make a coffee. Everything comes up in the correct order, with health checks at each and every step before moving to the next. This isn’t just automation; it’s liberation.
Secrets Management with SOPS: Security Meets Serenity 🔐
Now, I can already hear the security-conscious among you gasping. “But wait,” you cry, “what about the secrets?!” Fear not, dear reader, fear not. Of course, it goes without saying, but I’ll say it anyway, that you can’t just push secrets to a Git repository in plain text. That would be a recipe for absolute disaster! To address this critical concern, I leverage SOPS with age encryption: secrets are encrypted at rest in the repository and seamlessly decrypted by FluxCD at reconciliation time. Container registry credentials, database passwords, API tokens, all of them safely and securely stored in the repo alongside everything else, versioned, auditable, and locked up tight. Security and convenience, living together in perfect harmony. Who says you can’t have it all?
Chat Notifications: Your Cluster, Always in Your Pocket 📱
And here’s the cherry on top: FluxCD sends me chat notifications for every single deployment event. Every time a kustomization reconciles, a HelmRelease updates, or something fails, a message lands right on my phone. Is it a small thing? Perhaps. But it makes the whole setup feel truly alive, like the cluster is talking to me. I always know exactly what’s happening without ever having to check. Peace of mind, delivered in real time.
MetalLB on Bare Metal: Bringing Cloud Magic Home ✨
Let’s talk about one of the very first challenges that every aspiring home Kubernetes enthusiast will inevitably face on their journey: LoadBalancer services. In the cloud, your provider graciously hands you an external IP, and everything just works. At home? You get nothing. Nada. Your services just sit there in Pending state forever, dreaming of an IP address that will never come. Heartbreaking, right?
Enter MetalLB, the hero this story desperately needed and the unsung champion of bare-metal Kubernetes everywhere. Here’s exactly how I tamed the beast, piece by piece:
- 🎯 L2 address pool: A small range of IPs reserved on my home network, dedicated exclusively to LoadBalancer services.
- 🧹 Tidy advertisements: The L2 advertisement is configured to only announce from control-plane nodes, keeping everything neat and predictable.
- 🌐 Local DNS with an internal domain: Combined with MetalLB, I can access everything by name. No more remembering IPs, ever again!
Now, when I create a service of type LoadBalancer, MetalLB assigns it a real IP on my home network, just like the big cloud providers do. Truly a game-changer.
Longhorn: Distributed Storage That Just Works 🗄️
Ah, storage. Storage in Kubernetes is always a pain, and on bare metal, it’s even worse. It’s the final boss of every homelab journey. I needed a solution that could provide persistent volumes with robust replication across nodes, and after careful consideration, Longhorn emerged as the undisputed answer.
So what exactly does Longhorn bring to the table? It runs as a distributed block storage system across my amd64 nodes only. The Raspberry Pis, bless their hearts, simply don’t have the disk space or I/O performance to be useful as storage nodes, and that’s perfectly okay! Every node has its role to play in this grand orchestra. The local disks on the amd64 machines are where all the data lives, mounted at /var/lib/longhorn, humming along quietly and reliably, day in and day out.
My replication strategy is a masterpiece of pragmatism:
- 📦 Default replica count of 1 for most volumes: This is a home lab, after all, not a production data center!
- 🛡️ 3x replication for critical stuff like databases: Because some data is simply too precious to lose.
- 🎬 Media volumes stay at 1 replica: I’d rather have the performance, and let’s be honest, I can always re-download a movie.
And the icing on the cake? Longhorn comes with a gorgeous web UI that makes it effortless to monitor volume health, check space usage, and manage snapshots. Chef’s kiss. 👨🍳
Monitoring: Prometheus, Glances, and Grafana, Oh My! 📊
Let me ask you a question: what’s the point of running a cluster if you can’t obsessively stare at dashboards? Exactly. None whatsoever. That’s why I set up a full, comprehensive, no-compromises monitoring stack.
Prometheus: The Watchful Guardian 👁️
Here’s where I made a bold, some might even say contrarian, choice: I went with a hand-rolled Prometheus deployment instead of the ever-popular kube-prometheus-stack Helm chart. Gasp! I know, I know. But hear me out, because the reasoning is threefold and compelling: it’s lighter, I understand exactly what it does down to the last line of configuration, and for a home cluster, I simply don’t need all the complexity that comes with the full operator. Sometimes, less truly is more. Sometimes, the road less traveled is the right one.
Prometheus is pinned to a specific node with a 10Gi persistent volume on Longhorn and 7 days of retention. The scrape configuration is elegantly straightforward:
- 🌡️ Node Exporter for hardware and OS metrics from every node (CPU, memory, disk, temps)
- 📡 Blackbox Exporter for ICMP probes for network monitoring (yes, I even ping the IoT devices on my network to make sure they’re alive!)
- 🔄 Self-monitoring for both Prometheus and Blackbox Exporter
All of this rich telemetry feeds into stunning Grafana dashboards where I can see everything at a glance: node temperatures, disk usage, network throughput, service uptime. It’s like mission control, right in my browser.
Glances: htop for the Whole Fleet 🖥️
Glances runs as a DaemonSet with full host access (hostNetwork: true, hostPID: true), which means it’s deployed on every single node in the cluster and gives me a beautiful web-based view of system resources per machine. Think of it as htop in your browser, for every node. What’s not to love?
One delightfully fun quirk worth sharing: I had to exclude one of my nodes from the Glances DaemonSet because the Glances container has a compatibility issue with Debian 13 on that machine. But fear not! A little nodeAffinity rule takes care of that with surgical precision.
Node Exporter: The Unsung Hero 🦸
Node Exporter runs as a DaemonSet on every node (including the ARM ones!), tolerating all taints and exposing hardware metrics on port 9100. The temperature readings via the hwmon and thermal_zone collectors are particularly invaluable for keeping a watchful eye on the Raspberry Pis, which, as any seasoned Pi enthusiast will tell you, are notoriously known to thermal throttle when things heat up.
What’s Running on the Cluster? 🎪
At the end of the day, the whole point of this magnificent endeavor is to actually run stuff, right? And run stuff it does! The cluster proudly hosts a vibrant ecosystem of workloads:
- 🎬 Media servers for endless entertainment
- 📝 Content management systems for creative expression
- 🛠️ Custom web applications built with love
- 📊 Monitoring dashboards for that sweet, sweet observability
- 🕹️ File shares for retro gaming (yes, the Gameboy from my GBA post gets its ROMs from the cluster 😄)
All accessible via internal DNS. All managed by FluxCD. All with persistent storage on Longhorn. A seamless, self-healing, harmonious whole.
Lessons Learned: Wisdom from the Trenches 🎓
No transformative journey would be complete without a moment of quiet reflection. What did the trenches teach me? What would I whisper to my past self, standing there with a pile of hardware and a dream? Here are the hard-won insights I’ve gathered along the way, and I share them with you in the sincere hope that they’ll light your path as brightly as they’ve lit mine:
- 🌱 Start simple: I didn’t build all of this in one weekend, and neither should you! I started with K3s + MetalLB + one app, and kept adding layers as I got comfortable. Rome wasn’t built in a day, and neither was Magi.
- 🔄 GitOps makes a big difference: Having everything in one repo with FluxCD means I can review changes before they go live, roll back with a simple
git revert, and sleep soundly knowing the cluster matches what’s in Git. Priceless. - 🏗️ Mixed architectures work, but check your images: Most popular Docker images support multi-arch now, but every now and then you’ll hit one that’s amd64-only. Longhorn, for instance, only runs on my amd64 nodes.
- 📈 Monitoring from day one: Don’t wait until something breaks to set up Prometheus! The Raspberry Pis in particular need temperature monitoring. I’ve caught thermal throttling issues early thanks to the dynamic duo of Node Exporter + Grafana.
- 💾 Longhorn is great for home labs: It’s not the fastest storage solution out there, but it’s easy to set up, the UI is genuinely good, and having the option to replicate critical data across nodes gives real, tangible peace of mind.
What’s Next: The Road Ahead 🛣️
The journey never truly ends, does it? Here’s what’s on the horizon for Magi:
- 📊 Grafana dashboards: Building more custom dashboards for the services I’m running, because you can never have too many dashboards.
- 🔒 Cert-manager + external access: Exposing some services externally with proper TLS, security first!
- 💾 Backup automation: Longhorn snapshots flowing to an off-site NAS, because redundancy is love.
- 🥧 More Raspberry Pis: Because why not? Seriously, why not?
Conclusion: Your Cluster Awaits 🌅
And so, dear reader, we arrive at the end of our journey together. But is it really an end? Or is it, perhaps, a beginning?
In conclusion, this cluster is so much more than nodes, pods, and YAML files. It’s a testament to what’s possible when curiosity meets persistence, when late-night tinkering meets disciplined engineering. It’s a learning platform, a production environment, and a playground, all woven into one beautiful tapestry of self-hosted excellence. Every reconciliation loop, every scheduled pod, every replicated volume tells a story, and that story is one of growth.
If you’re thinking about building a home Kubernetes cluster, whether you’re a seasoned platform engineer with battle scars to prove it or a curious beginner taking your very first steps into the wondrous world of container orchestration, I wholeheartedly, enthusiastically, and unreservedly encourage you: give it a go. K3s makes it approachable. FluxCD keeps it sane. And the knowledge you’ll gain along the way? Absolutely priceless.
Thank you, truly and deeply, from the bottom of my heart, for joining me on this adventure. Your time and attention mean the world to me. The best is yet to come. Stay curious, keep building, and may your pods always be Running. 🚀✨👋