High-Availability k3s Cluster Guide with k3sup and kube-vip
Introduction
What is k3s?
k3s is a lightweight, fully compliant Kubernetes distribution designed for low-resource environments. It simplifies installation and management, making it perfect for development, testing, or small clusters.
Why k3sup?
k3sup (“ketchup”) is a simple CLI tool that installs k3s over SSH on any remote server. It handles the complexities of bootstrap, joining nodes, and copying kubeconfig automatically—ideal for lab setups or multi-node deployments.
Why kube-vip?
k3s HA clusters require a load balancer or a virtual IP to ensure the control plane remains reachable if a master node fails. kube-vip provides a lightweight, Kubernetes-native virtual IP that floats between master nodes, maintaining high availability without complex external load balancers.
Prerequisites
-
Nodes: At least 3 nodes (in this guide, we'll have all 3 nodes as the control plane and worker)
-
OS: We are going to use Debian but most Linux distro would be fine
-
Network: All nodes must communicate over a LAN
-
Tools installed locally:
sshaccess to all nodes,curlorwget -
Architecture note: Can mix ARM and x86_64 nodes. In my case, I currently have two Raspberry Pis and an old refurb ThinkPad X240. If you are using a Raspberry Pi, it's important that you edit cmdline.txt in /boot/firmware, then add:
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1. Those kernel parameters are related to Linux control groups (cgroups), which Kubernetes (and therefore K3s) relies on to manage resource allocation for containers.
Installation
Now let's get our hands dirty. With k3sup making our life easier, we can just configure everything from our local computer's command line. I would recommend you to spin up a Debian WSL if you are on Windows so you can have a Linux environment command line.
Install k3sup:
curl -sLS https://get.k3sup.dev | sh
sudo install k3sup /usr/local/bin/
k3sup uses ssh to connect to the server and agent nodes so we need to copy our public key. Generate a public key by executing:
ssh-keygen
Copy the generated ssh keys to your nodes, and don't forget to note where your ssh keys are generated:
ssh-copy-id sysad@192.168.100.201
ssh-copy-id sysad@192.168.100.202
ssh-copy-id sysad@192.168.100.203
Note: Make sure your Linux user for every node is not required to re-enter password. Add it to visudo by executing sudo visudo and adding the line:
[your-linux-user] ALL=(ALL) NOPASSWD:ALL
Install the kubectl (so you can run commands off your non k3s environment)
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
mkdir -p ~/.kube
We are now ready to install and configure your first master node:
k3sup install \
--ip 192.168.100.202 \
--user sysad \
--cluster \
--ssh-key ~/.ssh/id_ed25519 \
--tls-san 192.168.100.210 \
--local-path ~/.kube/config \
--k3s-extra-args "--disable servicelb --disable traefik"
--ip 192.168.100.202
Specifies the IP address of the target node where k3s will be installed.
-
This is the actual node IP, not the virtual IP
-
k3supconnects to this address over SSH to bootstrap k3s
--user sysad
The Linux user account used for the SSH connection.
-
This user must have passwordless sudo access
-
k3supuses sudo to install and configure k3s on the node
--cluster
Enables cluster mode instead of single-node mode.
-
Generates and stores a cluster join token
-
Allows additional control-plane and worker nodes to join later
-
Required for high-availability setups
--ssh-key ~/.ssh/id_ed25519
Explicitly specifies the SSH private key used to connect to the node.
-
Prevents
k3supfrom defaulting to~/.ssh/id_rsajust in case it is on another directory. -
Useful when using modern
ed25519keys or multiple SSH identities
--tls-san 192.168.100.210
Adds an extra Subject Alternative Name (SAN) to the Kubernetes API server TLS certificate.
-
This value should be the control-plane virtual IP (VIP)
-
Ensures the API server certificate remains valid when accessed via the VIP
-
Required when using kube-vip or any external load balancer
Without this option, accessing the API via the VIP would result in TLS errors.
--local-path ~/.kube/config
By default, k3sup generates the kubeconfig file in your current directory. We'll set this argument to have the file generated in ~/.kube/config, kubectl's default location. kubectl will find it automatically without needing any export command at all.
The command below will allow you to use the kubectl command, in case it is located in other that the default directory, or if you are having issues executing kubectl commands:
export KUBECONFIG=path/to/kubeconfig
--k3s-extra-args "--disable servicelb --disable traefik"
Passes additional arguments directly to the k3s server.
In this setup:
-
servicelbis disabled because kube-vip is used as the service load balancer -
traefikis disabled to allow installation of a custom ingress controller later (e.g., NGINX, HAProxy, or Traefik deployed manually)
Disabling unused components keeps the control plane lean and predictable.predictable.
Before we join the rest to the cluster, it's nice to have kube-vip installed and configured first.
Apply RBAC permissions:
kubectl apply -f https://kube-vip.io/manifests/rbac.yaml
This command ensures kube-vip has the required roles and permissions in the cluster by creating a ServiceAccount, ClusterRole, and ClusterRoleBinding. It’s necessary before deploying kube-vip as a DaemonSet or LoadBalancer controller.
Let's set an alias "kube-vip" for a cleaner command:
alias kube-vip="sudo ctr run --rm --net-host docker.io/plndr/kube-vip:latest vip /kube-vip"
Or if you have docker engine in your machine:
alias kube-vip="sudo docker run --network host --rm ghcr.io/kube-vip/kube-vip:main"
Apply manifest:
kube-vip manifest daemonset \
--interface eth0 \
--address 192.168.100.210 \
--inCluster \
--controlplane \
--services \
--arp \
--leaderElection | ssh sysad@192.168.100.202 "sudo tee /var/lib/rancher/k3s/server/manifests/kube-vip.yaml"
Now we can join the rest of the nodes:
# We can now use the virtual IP we set earlier as the server ip (--server-ip) :)
k3sup join --ip 192.168.100.201 --user sysad --server --ssh-key ~/.ssh/id_ed25519 --server-ip 192.168.100.210 --server-user sysad
k3sup join --ip 192.168.100.203 --user sysad --server --ssh-key ~/.ssh/id_ed25519 --server-ip 192.168.100.210 --server-user sysad
Open your local kubeconfig file your local terminal, which is the Debian WSL in my case.
sudo nano kubeconfig
Change server: In my case, from https://192.168.100.202:6443 to the floating/virtual IP https://192.168.100.210:6443.
Now, your WSL terminal talks to the cluster via the Floating IP as well.
That's it! You now have a highly-available k3s cluster. We'll soon dive in and test it's HA and deploy our first app. Stay tuned!

