Skip to content
Go back

Cluster API for Homelab k3s

By KingPin 11 min read
Cluster API for Homelab k3s
Contents

Your k3sup Script Doesn’t Scale Past One Cluster

You built your 3 node k3s cluster with k3sup, or kubeadm, or an Ansible playbook you’ve been copy pasting since 2024. It works. You SSH in twice a year to bump the k3s version and otherwise leave it alone. Good for you, that’s the correct amount of effort for one homelab cluster.

Now you’ve got a second cluster for testing. And a third because your job wants you fluent in the same tooling they run in production. Suddenly you’re maintaining three different sets of scripts, three different mental models for “how did I build this one again,” and the answer to “what k3s version is cluster two running” requires SSH and grep instead of a git diff.

Cluster API (CAPI) is Kubernetes’ answer to that problem, but one layer up from where most homelab guides stop. k3sup and kubeadm get nodes joined into a cluster. Cluster API treats the whole cluster (control plane, worker nodes, scaling, version upgrades) as objects a controller reconciles, the same way a Deployment reconciles your pod count. You stop running commands and start committing YAML.

If you already run ArgoCD for homelab GitOps (we covered that setup in an earlier post), this is the natural next commit in the same repo: instead of ArgoCD only syncing what runs inside your cluster, the cluster itself becomes a manifest ArgoCD can sync too. Same git history, same pull request review, now covering the infrastructure layer as well as the app layer.

What Cluster API Actually Is

Cluster API needs a management cluster: a small Kubernetes cluster whose only job is running the CAPI controllers and holding the custom resources that describe your other clusters. Confusing part first: yes, you need a working Kubernetes cluster to manage your Kubernetes clusters. kind or k3d is the normal way to stand one up, and it can be a throwaway VM since it’s not running your workloads.

Once the management cluster exists, clusterctl installs the controllers and you start creating objects. The core CRDs from the kubernetes-sigs/cluster-api project (current release v1.14.2) are infrastructure agnostic: a Cluster object ties everything together, and MachineDeployment objects describe groups of worker nodes with a replica count, exactly like a Deployment describes a group of pods.

The default bootstrap and control plane providers that ship with CAPI are kubeadm based, which makes sense since kubeadm is what upstream Kubernetes uses. But you’re running k3s, and k3s doesn’t bootstrap itself with kubeadm. k3s-io/cluster-api-k3s fills that gap with a pair of providers built specifically for k3s.

Every CAPI provider works the same way under the hood: a controller watches a CRD, compares what exists against what the spec says should exist, and reconciles the difference. Change a replica count, and the MachineDeployment controller notices the drift and creates or deletes machines until reality matches the manifest. That reconciliation loop is the entire reason this beats a script: a script runs once and stops caring, a controller keeps checking forever. If someone (you, at 1am, “just testing something”) deletes a worker node’s container by hand, CAPI notices the machine is gone and rebuilds it, the same way a Deployment replaces a pod you killed with kubectl delete pod.

The CRD you’ll actually write is KThreesControlPlane (yes, spelled with “Three,” it’s a k3s pun that made it into production code) and KThreesConfigTemplate for worker bootstrap config. Both live at the v0.4.0 release tag as of this writing. The project’s own README is upfront that it’s early stage and assumes you already know Cluster API, which is honest and also the exact right amount of warning label.

One thing to keep straight: RKE2 has its own separate provider, rancher/cluster-api-provider-rke2. If you go looking for “Cluster API RKE2” tutorials and start copying manifests into a k3s setup, they won’t match. Different distro, different bootstrap provider, different CRD names. Don’t mix them.

Picking an Infrastructure Provider

Cluster API needs an infrastructure provider too, the piece that actually creates VMs or bare metal machines and hands their IPs back to the bootstrap provider. For a real homelab deployment on Proxmox, ionos-cloud/cluster-api-provider-proxmox (CAPMOX, currently at v0.9.1) is the community option that’s actively maintained and talks to the Proxmox API to spin up VMs from a template.

But don’t start there. Start with the Docker infrastructure provider (CAPD), which ships inside the core cluster-api repository and fakes machines as Docker containers on whatever box you’re already running the management cluster on. It’s explicitly called out in the docs as not for production, and that’s fine, because you’re not trying to run production. You’re trying to learn what a Cluster object does before you spend a Saturday debugging Proxmox networking on top of it.

Standing Up the Management Cluster

Create a kind cluster that can talk to the Docker socket, since CAPD needs it to spin up “machine” containers:

setup.sh
cat > kind-cluster-with-extramounts.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: capi-quickstart
nodes:
- role: control-plane
extraMounts:
- hostPath: /var/run/docker.sock
containerPath: /var/run/docker.sock
EOF
kind create cluster --config kind-cluster-with-extramounts.yaml

Point clusterctl at the k3s providers. Drop this in $HOME/.config/cluster-api/clusterctl.yaml:

clusterctl.yaml
providers:
- name: "k3s"
url: "https://github.com/k3s-io/cluster-api-k3s/releases/v0.4.0/bootstrap-components.yaml"
type: "BootstrapProvider"
- name: "k3s"
url: "https://github.com/k3s-io/cluster-api-k3s/releases/v0.4.0/control-plane-components.yaml"
type: "ControlPlaneProvider"

Then initialize everything in one command:

Terminal window
export CLUSTER_TOPOLOGY=true
clusterctl init --bootstrap k3s --control-plane k3s --infrastructure docker

Give it a minute, then check the controllers landed:

Terminal window
kubectl get pods -A

The Manifest That Replaces Your Ansible Playbook

This is the part that used to be a 40 line Ansible playbook or a shell script full of k3sup install --ip flags. Now it’s six CRDs in one YAML file. Trimmed to the parts that matter:

cluster.yaml
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: homelab-01
spec:
controlPlaneRef:
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KThreesControlPlane
name: homelab-01-control-plane
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerCluster
name: homelab-01
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KThreesControlPlane
metadata:
name: homelab-01-control-plane
spec:
replicas: 1
version: v1.30.2+k3s2
machineTemplate:
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerMachineTemplate
name: homelab-01-control-plane
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: homelab-01-md-0
spec:
clusterName: homelab-01
replicas: 2
template:
spec:
clusterName: homelab-01
version: v1.30.2+k3s2
bootstrap:
configRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
kind: KThreesConfigTemplate
name: homelab-01-md-0
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerMachineTemplate
name: homelab-01-md-0

Notice the Kubernetes version string carries the +k3s2 suffix, not a plain semver. That’s k3s’s own release tagging, and CAPI passes it straight through to the bootstrap script. Generate the full manifest set (including the DockerMachineTemplate and KThreesConfigTemplate objects the snippet above references) from the project’s own template instead of hand assembling it:

Terminal window
export KIND_IMAGE_VERSION=v1.30.0
clusterctl generate cluster --from samples/docker/cluster-template-quickstart.yaml \
homelab-01 --kubernetes-version v1.30.2+k3s2 \
--worker-machine-count 2 --control-plane-machine-count 1 > cluster.yaml
kubectl apply -f cluster.yaml

Watch the machines come up:

Terminal window
kubectl get machine

Once they’re ready, grab the workload cluster’s kubeconfig the same way you’d grab it from k3sup, just through a different command:

Terminal window
clusterctl get kubeconfig homelab-01 > workload-kubeconfig.yaml

Scaling by Editing One Number

Here’s the payoff. Adding a worker node used to mean re-running an install script against a new IP and hoping the token hadn’t rotated. Now it’s a one line change to a field that already exists:

Terminal window
kubectl scale machinedeployment homelab-01-md-0 --replicas=4

MachineDeployment supports the scale subresource, so that command works exactly like scaling a Deployment. CAPI notices the replica count changed, creates two more DockerMachineTemplate backed containers, bootstraps them with CABP3, and joins them to the cluster. No inventory file, no new host entry, no re-running a playbook against the whole fleet to add one node. If you’d rather keep the change in git, bump spec.replicas in cluster.yaml and kubectl apply it, same result, now with a commit history of every scaling decision you’ve made.

Upgrading k3s as Code

Upgrading used to mean SSHing to each node and running the k3s installer again, in the right order, hoping you didn’t forget the control plane node last (you did, once, we’ve all done it). With CAPI, you change the version field:

Terminal window
kubectl patch kthreescontrolplane homelab-01-control-plane \
--type merge -p '{"spec":{"version":"v1.37.0+k3s1"}}'

CACP3 rolls the control plane machines one at a time, replacing each with a fresh machine on the new version rather than upgrading in place. Bump the same version field on the MachineDeployment and it does the equivalent rolling replacement for workers. Replacing machines instead of patching them in place means a bad k3s release doesn’t leave you with a half upgraded node you have to hand debug: the old machine stays up until the new one is healthy, and if the new one never comes up healthy, the rollout just stalls instead of taking your control plane down. Upgrades become a diff in a manifest, reviewable in a pull request, instead of a runbook you hope you followed correctly at 11pm.

Tearing the whole thing down is one command too, and CAPI is specific that you delete the top level object, not the pieces underneath it:

Terminal window
kubectl delete cluster homelab-01

When This Is Overkill (Which Is Often)

Our own k3sup vs kubeadm post made the case that simple tools win for a single 3 node homelab cluster, and nothing here contradicts that. A management cluster is another thing that can fall over at 2am. The k3s provider is early stage by its own admission, the CRD surface is bigger than a shell script, and you’re now debugging two clusters (management and workload) when something breaks instead of one.

There’s one nuance worth being precise about: if the management cluster goes down, your existing workload clusters keep running. CAPI only intervenes when a controller needs to reconcile a change, so a dead management cluster means you can’t scale or upgrade until it’s back, not that your homelab loses its cluster mid Netflix binge. That’s a real failure mode, just a slower burning one than “the cluster is down.”

If you have exactly one cluster, built once, touched twice a year for version bumps: keep k3sup. You don’t need a reconciliation loop to run one kubectl apply a year.

Cluster API earns its complexity once you’re managing more than one cluster, or once “how do I stand up a cluster” is a skill you want to carry into a job that already runs CAPI in production. The muscle memory of Cluster, MachineDeployment, and rolling upgrades through CRDs transfers directly to AWS, Azure, and vSphere backed clusters at work, since the core objects don’t change, only the infrastructure provider does. That’s a real reason to spend a weekend on CAPD even if you never touch Proxmox with it. It’s not a replacement for your homelab script, it’s a different tool for a different number of clusters.

Common Questions

Does Cluster API replace k3sup or kubeadm for building a single cluster?

Not for a single cluster you rarely touch. k3sup and kubeadm remain faster for standing up one homelab cluster once. Cluster API adds a management cluster and a controller loop, worth the overhead once you’re managing multiple clusters or want lifecycle changes (scaling, upgrades) tracked as code instead of scripts.

Can I try Cluster API without buying a Proxmox box or cloud account?

Yes. The Docker infrastructure provider (CAPD) ships inside the core kubernetes-sigs/cluster-api repository and runs “machines” as Docker containers on your existing management cluster host. It’s explicitly documented as unsuitable for production, but it’s the fastest way to learn the CRD model before touching real hardware.

What k3s versions does the cluster-api-k3s provider support?

The provider passes the version string straight to k3s’s own release tags, including the +k3s1 style suffix, so it tracks whatever k3s ships. As of September 2026 the current k3s release is v1.37.0+k3s1. Always confirm the exact tag on the k3s-io/k3s releases page before setting spec.version.

Is cluster-api-provider-proxmox ready for a real homelab deployment?

Yes, with caveats. ionos-cloud/cluster-api-provider-proxmox (CAPMOX) is actively maintained, at v0.9.1 as of September 2026, and is the community standard for provisioning VMs on Proxmox through Cluster API. It’s still pre 1.0, so pin versions and test upgrades on a throwaway cluster first.

Do I need a dedicated always-on management cluster?

Yes, CAPI’s controllers need somewhere to run and reconcile. It does not need to be powerful or highly available for a homelab: a small VM or even a kind cluster on a machine you already run works, since it only stores CRDs and runs lightweight controllers, not your actual workloads.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
OPNsense Multi-WAN Failover Guide
Next Post
RabbitMQ vs NATS: Pick a Message Bus

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts