Why k3s? Choosing and Installing k3s for Your Homelab

June 11, 2026

Some of the links in this post may be affiliate links. If you click through and make a purchase, I may earn a commission at no extra cost to you.

You picked your architecture. If you haven't, read the architecture comparison guide first — it'll take ten minutes and save you from building the wrong thing. If you know you're building three nodes with kube-vip, let's start installing k3s.

By the end of this post, you'll have a fully HA cluster: three machines running k3s with embedded etcd, kube-vip floating a virtual IP between them, kubectl pointed at the VIP, and every node joined through it. Node 1 goes down? kubectl keeps working. That's actual high availability — not just replicated data with a hardcoded IP waiting to fail. If you're only installing one node, that's fine too — install Node 1, deploy kube-vip anyway, and when you add nodes later they join through the VIP with zero retrofit. Same commands, same manifest, same outcome.

No theory — that's the next post. This one is terminal output and a working cluster.

Key Takeaways

  • k3s is the correct Kubernetes distribution for homelab — 1 vCPU and 512MB RAM on agent nodes, 2 vCPU and 2GB RAM on Control Plane nodes, single binary, embeds everything you need (etcd, containerd, CoreDNS), and installs in under 60 seconds per node
  • Installing k3s is five steps: Node 1 with --cluster-init to bootstrap embedded etcd → kube-vip DaemonSet to float the VIP → update kubeconfig to the VIP → Nodes 2 and 3 join through the VIP → all three nodes Ready, kubectl survives any node failure. Every node gets --disable servicelb --disable traefik because MetalLB and Helm-managed Traefik replace them later.
  • Disable multipathd BEFORE installing k3s — it's enabled by default on Ubuntu Server and will silently claim Longhorn block devices later, causing CSI mount failures that are nearly impossible to diagnose
  • If you're installing a single node: deploy kube-vip anyway. On one node there's no failover, but your kubeconfig already points at the VIP. When you add nodes later, they join through the VIP and kube-vip is already running. No retrofit. No "wait, which IP did I use?" Six months later.

Table of Contents


Why k3s? (And Why Not kubeadm, microk8s, or Talos)

k3s is the correct Kubernetes distribution for a homelab because it was purpose-built for resource-constrained environments, ships with everything you need, and installs in under 60 seconds. Kubernetes development activity now trails only the Linux kernel in total commit volume — a 34-year-old project (CNCF State of Cloud Native, February 2026). k3s is a CNCF-certified Kubernetes distribution trusted in production edge deployments across retail, telecom, and manufacturing. This isn't a toy distribution. It's a production platform that happens to fit on hardware you already own.

But there are other distributions. Here's why k3s wins for homelab specifically:

The Competition — Quick Disqualification

kubeadm is the "official" Kubernetes installer. It's the right choice for CKA study and enterprise deployments. For homelab, it's the wrong tool. kubeadm requires 2 vCPUs minimum for control plane nodes and takes 10+ minutes to bootstrap — vs. k3s's sub-60-second install. It ships with nothing: no ingress controller, no service load balancer, no storage provisioner. You install every component yourself. That's power in a datacenter. It's overhead in a homelab.

microk8s is Canonical's Kubernetes distribution. It runs as a snap package, which means automatic updates can break your cluster at the worst possible moment. It runs its own containerd and fights with Docker if you have it installed. microk8s is fine for a Raspberry Pi quick test. For a production homelab you intend to run for years, the snap lifecycle alone is disqualifying.

Talos Linux is the coolest Kubernetes distribution you'll never SSH into. It's immutable, API-driven, and designed for operators who want to treat their OS like a Kubernetes resource. It's also a completely new operating system — no apt, no SSH troubleshooting, no Stack Overflow answers that assume Ubuntu. If you already know Ubuntu, start there. Talos can be a future upgrade. It shouldn't be your first cluster.

k0s is the closest competitor — single binary like k3s, similarly resource-efficient. But In 2026, k0s had 8,100 GitHub stars vs. k3s's 33,100+ (GitHub, May 2026). That community size difference matters when you're troubleshooting at 11pm and searching for answers.

The Comparison Table

k3skubeadmmicrok8sTalosk0s
Min CPU (agent)1 core1 core1 core2 cores1 core
Min CPU (control plane)2 cores2 cores2 cores2 cores2 cores
Min RAM (agent)512MB2GB2GB2GB1GB
Install time< 60s10+ min5 min15 min< 60s
Built-in ingressYes (Traefik)NoYes (NGINX)NoNo
Built-in LBYes (ServiceLB)NoYes (MetalLB)NoNo
Embedded etcdYesNoYesNo (external only)Yes
GitHub stars33.1kN/A (part of k8s)8.5k7.3k8.1k
Homelab score (1–5)52324

Sources: k3s docs (linked above), kubeadm docs, Talos docs. Homelab score is my assessment based on resource efficiency, install simplicity, and operational overhead — not a published metric.

One note: we're DISABLING k3s's bundled Traefik and ServiceLB at install time. Why? Because we're installing Traefik via Helm (full control over Helm values) and MetalLB (L2/ARP mode for bare-metal LoadBalancer IPs) in later posts. k3s-bundled Traefik doesn't expose Helm values, and k3s-bundled ServiceLB conflicts with MetalLB. Disable them now. Install the real versions later.


What Hardware and OS Do You Need for a k3s HA Homelab?

A production k3s HA cluster starts with three nodes. You can run three mini PCs (under $600 total), three VMs on existing hardware, or a mix. The minimum per control plane node: 2 CPU cores, 2GB RAM, 32GB storage (k3s docs, 2025). Agent nodes can get away with 1 core and 512MB RAM, but since our three nodes are all control plane (we covered why in the architecture comparison), budget for 2 cores and 4GB+ RAM each.

Ubuntu Server 24.04 LTS is the recommended OS — not because it's special, but because you already know it, it's stable, and every troubleshooting guide on the internet assumes Debian/Ubuntu.

The Hardware

I like BeeLink S12 Pro mini PCs: Intel N100, 16GB DDR5 RAM, 512GB NVMe. These go for $150–180 each on Amazon as of May 2026 — under $550 total for a three-node HA control plane. Add a small UPS so a power blip doesn't take down all three nodes simultaneously.

Three VMs on a single Proxmox host work identically for learning. The install commands are the same. The only difference: if the Proxmox host goes down, all three "nodes" go down together — you lose physical fault isolation. For a production homelab, spread across physical machines. For learning, VMs are perfect. Start with what you have.

OS Prep

Static IPs on all three nodes — configure these at Ubuntu install time or via DHCP reservations on your router. All nodes must be on the same subnet (ex. 192.168.5.xxx). SSH key access from your workstation to all three nodes is recommended.

The multipathd gotcha. multipathd is enabled by default on Ubuntu Server. It claims block devices via device-mapper. When Longhorn (deployed in Section 4) tries to mount a CSI volume, multipathd may have already claimed the device and the mount fails with an error message that tells you absolutely nothing about multipathd.

I learned this the hard way. It's better to learn from me instead.

# Run on EVERY node before installing k3s
sudo systemctl disable --now multipathd.service multipathd.socket

Thirty seconds now saves you an evening of "why won't my volumes mount" later. This is in the real repo's bootstrap README for a reason. If you want to read about the "fun" time I had with it, I documented it in the repo here

Network check: All nodes can ping each other. All nodes can reach the internet. No firewall rules between nodes on the cluster network. Your router's DHCP range does NOT include the IPs you reserved for kube-vip and MetalLB (the IP planning post covers this in detail — read it before you install).


How Do You Install Node 1?

Installing k3s is one command. Node 1 bootstraps the embedded etcd cluster. In the next section, you'll deploy kube-vip to float a VIP. Then Nodes 2 and 3 join through that VIP. Under 60 seconds per node.

Generate a Token

The token is the shared secret that allows nodes to join the cluster. Generate it once, use it everywhere. Store it in your password manager — you'll need it if you add nodes later.

cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32
# Output: something like a8Xk3mPqL7rT2vY9wB4nD6jF1hC5sZ

Save that string. You'll paste it into every install command.

Node 1 — Bootstrap the Cluster

curl -sfL https://get.k3s.io | K3S_TOKEN="<YOUR_TOKEN>" \
  INSTALL_K3S_EXEC="server \
    --cluster-init \
    --disable servicelb \
    --disable traefik \
    --tls-san <KUBE_VIP_IP>" \
  sh -

Every flag matters. Here's what each one does:

FlagWhat it doesWhy it matters
--cluster-initBootstraps the embedded etcd cluster on this nodeRequired for HA. Without it, etcd runs single-node and you can't add control plane members later.
--disable servicelbDisables k3s's built-in Klipper ServiceLBMetalLB replaces this in a later post. Both active = ARP conflicts = intermittent connection failures. This cannot be un-done after install.
--disable traefikDisables k3s's bundled TraefikWe install Traefik via Helm in a later post for full control over Helm values. k3s-bundled Traefik doesn't expose its Helm values — you get the defaults, forever.
--tls-sanAdds a Subject Alternative Name to the API server's TLS certificateMust include your kube-vip VIP. Without it, kubectl works over Node IPs but fails over the VIP. This CANNOT be changed after install without reinstalling the entire cluster.

If you're building a single-node learning cluster (no HA) and don't plan on adding any more nodes in the future, drop the --cluster-init flag. If you're not sure if you'll be adding more nodes, than keep everything as-is - you'll deploy kube-vip in the next section, and if you add nodes later, your TLS cert already includes the VIP.

Wait for Node 1 to finish. Watch the progress:

# On Node 1
sudo journalctl -u k3s -f

You'll see etcd starting, the API server coming up, and eventually the node registering itself. When the log settles, confirm:

sudo k3s kubectl get nodes
# NAME     STATUS   ROLES                       AGE   VERSION
# node1    Ready    control-plane,master        30s   v1.35.5+k3s1

One node, Ready. Let's get kubectl working.

Get kubectl Working (Temporary)

k3s writes its kubeconfig to /etc/rancher/k3s/k3s.yaml on every control plane node. Copy it to ~/.kube/config on your workstation.

# On Node 1
sudo sed 's/127.0.0.1/<NODE1_IP>/g' /etc/rancher/k3s/k3s.yaml

Copy the output.

On your workstation:

mkdir -p ~/.kube
# Paste the output into ~/.kube/config
chmod 600 ~/.kube/config

Verify (on your workstation):

kubectl get nodes -o wide
# NAME     STATUS   ROLES                       AGE   VERSION          INTERNAL-IP
# node1    Ready    control-plane,master        2m    v1.35.5+k3s1    192.168.5.100

A quick check on what's actually running:

kubectl get pods -A
# NAMESPACE     NAME                                      READY   STATUS
# kube-system   coredns-xxxxx-x                           1/1     Running
# kube-system   local-path-provisioner-xxxxx-x            1/1     Running
# kube-system   metrics-server-xxxxx-x                    1/1     Running

That's it. k3s core components: CoreDNS, the local-path storage provisioner, and the metrics server. No ServiceLB (we disabled it). No Traefik (we disabled it). No workloads. Just a clean, minimal cluster.

Security note: The kubeconfig gives full cluster admin access. chmod 600 ~/.kube/config. Never commit it to git — the homelab-k8s repo's .gitignore already excludes it.

This kubeconfig points at Node 1's IP. If Node 1 goes down, kubectl stops working. That's temporary — the next section fixes it permanently.

What Just Happened

k3s downloaded its single binary (~60MB), created systemd units, started the kubelet, and bootstrapped embedded etcd. The entire process took under 60 seconds. kubeadm would still be downloading container images.


How Do You Install kube-vip?

Node 1 is running. Now make cluster access highly available — even if you only have one node right now. kube-vip floats a virtual IP between control plane nodes using ARP. Apply the DaemonSet once, update your kubeconfig to the VIP, and kubectl survives any single node failure.

If you're installing a single node: Deploy kube-vip anyway. On one node there's no failover — the VIP just sits on that node. But your kubeconfig already points at the VIP. When you add nodes later, they join through the VIP and kube-vip is already running. No retrofit. No "wait, which IP did I use?" Six months later. The manifest is identical. The commands are identical. Do it now.

Interface Detection

Leave vip_interface blank in the DaemonSet — kube-vip auto-detects the primary interface. No manual discovery needed. If your nodes have different interface names (mixed hardware), systemd link files rename them consistently. Better: buy identical nodes. The repo cluster runs three identical BeeLink EQ12s — same interface name on all three.

Deploy the DaemonSet

kube-vip runs as a DaemonSet — one pod per control plane node. Two environment variables control everything and are defined in the manifest:

Env VarWhat it isExample
vip_addressThe virtual IP that floats between nodes192.168.5.201
vip_interfaceThe physical NIC the VIP binds to`` (leave blank to autodetect the primary interface)

The DaemonSet manifest lives in the homelab-k8s repo. Clone it, edit those two env vars for your network, and apply:

kubectl apply -f bootstrap/kube-vip/daemonset.yaml
# daemonset.apps/kube-vip created
kubectl apply -f bootstrap/kube-vip/rbac.yaml

kube-vip needs hostNetwork: true and privileged access — it manipulates the host's network stack directly (adds the VIP as a secondary IP, sends ARP packets). This is expected for any network-level DaemonSet. MetalLB and Cilium require the same. The deep-dive post (M3) explains the ARP mechanics in detail.

Verify kube-vip Is Working

kubectl get pods -n kube-system -l app=kube-vip
# NAME             READY   STATUS    RESTARTS   AGE
# kube-vip-xxxxx   1/1     Running   0          30s

On the leader node, the VIP should appear as a secondary IP on your physical interface:

ip addr show enp1s0 | grep <VIP>
# inet 192.168.5.201/32 scope global enp1s0

Point kubectl at the VIP

Now the permanent fix. Update your kubeconfig's server: field from Node 1's IP to the VIP (Replace the values in the command below to the correct ones):

# On your workstation
sed -i 's/<NODE1_IP>/<VIP>/g' ~/.kube/config

Verify kubectl works through the VIP:

kubectl get nodes
# NAME     STATUS   ROLES                       AGE   VERSION
# node1    Ready    control-plane,master        5m    v1.35.5+k3s1

It works. kubectl is talking to the VIP, not Node 1. If Node 1 goes down, the VIP floats to another node and kubectl keeps working. That's actual HA — not just replicated data with a hardcoded IP waiting to fail.

Now add the other nodes. Through the VIP.


How Do You Add Nodes 2 and 3?

Same command on both nodes. One critical change: --server points at the VIP, not Node 1's IP.

curl -sfL https://get.k3s.io | K3S_TOKEN="<YOUR_TOKEN>" \
  INSTALL_K3S_EXEC="server \
    --server https://<VIP>:6443 \
    --disable servicelb \
    --disable traefik \
    --tls-san <KUBE_VIP_IP>" \
  sh -

Same flags, same token. The node contacts the VIP to find the cluster. Why the VIP instead of Node 1's IP? Because any surviving control plane node can accept the join request. If Node 1 reboots mid-join, the VIP floats to Node 2 and the join continues — no manual intervention, no waiting, no "which node was I pointing at?" With Node 1's IP, the join fails and you start over. With the VIP, it Just Works.

Wait for each node to finish (watch the journal if you want), then verify:

kubectl get nodes
# NAME     STATUS   ROLES                       AGE   VERSION
# node1    Ready    control-plane,master        10m   v1.35.5+k3s1
# node2    Ready    control-plane,master        2m    v1.35.5+k3s1
# node3    Ready    control-plane,master        30s   v1.35.5+k3s1

Three nodes. All joined through the VIP. All Ready. Your kubeconfig points at the VIP. If any node fails, the VIP floats to a survivor and kubectl keeps working. That's a fully HA cluster — in under five minutes from bare metal to production-ready control plane.


What Does Your k3s HA Cluster Have (and What's Still Missing)?

You have a running 3-node k3s cluster with embedded etcd. kubectl works. The control plane is healthy. Here's what's running, and what's still missing:

ComponentStatusWhat It ProvidesPost
k3s coreInstalledAPI server, etcd, kubelet, containerd, CoreDNSThis post
Control plane VIPInstalledkubectl survives any node failureThis post
TLS certificatesNot yetHTTPS for all servicescert-manager (future)
Service LoadBalancer IPsNot yetExternal IPs for LoadBalancer servicesMetalLB (future)
Ingress controllerNot yetHTTP/S routing to appsTraefik (future)
GitOpsNot yetDeploy everything via git pushArgoCD (future)

This gap map is the curriculum. Every one of those missing pieces is a dedicated post that builds on the foundation you just installed. By the end of the series, every row in that table will be green.

The pillar page — Production k3s HA In Your Homelab: The Complete Architecture — explains why these components must be deployed in this EXACT order. Short version: each one depends on the previous. Deploy them out of order and you'll spend hours debugging errors that point at the symptom, not the cause.


Post-Install — What to Do Right Now

Before you move on to the next component, do four things. These are the things you'll wish you did when you rebuild this cluster in six months.

1. Back Up the Token

The k3s token is in /var/lib/rancher/k3s/server/token on any control plane node. Copy it. Store it in your password manager.

sudo cat /var/lib/rancher/k3s/server/token

Without this token, you can't add nodes to the cluster. It's a single string — copy-paste it somewhere safe.

2. Take VM Snapshots

If you're on VMs (Proxmox, ESXi), snapshot all three nodes now. This is your "clean install" checkpoint — if MetalLB or Traefik configuration goes sideways, you can revert to here in seconds instead of reinstalling from scratch.

If you're on bare metal, at minimum back up the token and note the k3s version (kubectl version --short). You can't snapshot a physical machine, but you CAN document exactly what you installed so a rebuild is fast.

3. Bookmark the Repo

github.com/Taegost/homelab-k8s — every manifest, every Helm value, every troubleshooting doc referenced in this series lives there. The bootstrap README mirrors the install commands in this post. If something doesn't match, the repo is the authoritative reference — it's updated more often than blog posts.

4. Leave a Note for Your Future Self

Create a text file somewhere off-cluster (your notes app, a git repo, whatever you use). Write down:

  • The three node hostnames and IPs
  • The k3s version you installed (the output of kubectl version --short)
  • The kube-vip VIP (the one in your kubeconfig)
  • The token location (your password manager)

Six months from now, when you're debugging a weird etcd issue at 11pm, this note will save you twenty minutes of "wait, which node was .100 again?"


Frequently Asked Questions

"Why disable Traefik and ServiceLB? Can't I remove them later?"

No. k3s bundles Traefik and Klipper ServiceLB as built-in components. There's no clean uninstall — you disable them at install time or they're there forever, consuming resources and potentially conflicting with MetalLB and Helm-installed Traefik. The --disable flags are permanent. Get them right now and you never think about them again.

"Can I start with one node and add more later?"

Yes. Follow the Node 1 + kube-vip steps above. Skip Nodes 2 and 3. kube-vip runs on your single node — no failover yet, but your kubeconfig already points at the VIP. When you add nodes later, they join through the VIP with --server https://<VIP>:6443 and kube-vip is already running. No retrofit required. No reinstalling. No "wait, which IP did kubeconfig point at?" A single-node etcd cluster has no replication, and the join process carries a small risk of quorum disruption — take a snapshot before adding nodes. But the architecture is explicitly upgradeable. You don't have to start at the destination.

"What if I don't have three physical machines?"

Three VMs on a single Proxmox host work identically. The install commands above are the same whether the "machine" is physical or virtual. The only difference: if the VM host goes down, all three nodes go down together. For learning, VMs are perfect. For a production homelab, spread across physical machines. Start with what you have.

"Why install kube-vip before joining additional nodes?"

Because joining through the VIP means cluster access survives any single node failure from the moment the cluster is complete. If you join Nodes 2 and 3 through Node 1's IP and Node 1 reboots mid-join, the join fails. Through the VIP, the VIP floats to Node 2 and the join continues. The VIP also means your kubeconfig — which you updated before adding nodes — already points at a floating IP. No "wait, which node did I point kubectl at?" Six months later. Five extra minutes during install. Zero retrofit forever.

"I got an error about port 6443 already in use — now what?"

Something is bound to the Kubernetes API port. Check with sudo lsof -i :6443. The most common culprit: a previous k3s install that wasn't fully cleaned up. k3s ships an uninstall script at /usr/local/bin/k3s-uninstall.sh — run it, reboot, and try again. If the port is claimed by another service, move that service or change k3s's port with --https-listen-port <new_port> (and update the --server flag on subsequent nodes to match).


Next: How kube-vip Actually Works

You have a fully HA cluster. kube-vip is running. kubectl points at the VIP. If Node 1's power brick dies, kubectl keeps working.

But HOW does that VIP float between nodes? What's happening at the packet level? The next post — What kube-vip Actually Does: Control Plane HA and IP Planning for k3s — is the deep dive. ARP mechanics, gratuitous ARP, leader election via Kubernetes Leases, why DaemonSet beats static pods, and the kube-vip vs MetalLB layer distinction everyone gets wrong. This post was the "run these commands." That post is the "here's what those commands built and why." Both matter. Read them in order.

Don't be afraid to break things. You have snapshots. You have the repo. You have a token in your password manager. The worst case is a reinstall, and you just proved you can do that in under five minutes.

Just do something.

Sources

I'm Mike Wheway. I run the homelab-k8s cluster (linked in the Post-Install section above) — 18+ apps on three mini PCs, everything defined in Git, everything deployed via ArgoCD. The commands in this post are the exact commands that built it. If something doesn't match, the repo is the source of truth. If you find a better way, submit a PR.

If you found this helpful, or if you hit a weird error I didn't cover, let me know in the comments. I keep a troubleshooting doc in the repo specifically because I want to capture the things that go wrong — they're more valuable than the things that go right.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.