K3s installs on Debian 13 without a single extra package, and the one warning it prints at startup is noise you can safely ignore. To install K3s on Debian you need one script from the stable channel, a firewall you open on purpose, and a worker that joins with a token. What matters on Trixie is which iptables backend K3s writes to, why nft-expr-counter fails to load, and what k3s check-config complains about.
You’ll build a two-node cluster on Debian 13, put a test app behind the bundled Traefik ingress and turn on secrets encryption on the running cluster. The Traefik chart v40 key rename that silently breaks older HelmChartConfig files gets its own section. Every command below was run on Debian 13 with K3s v1.36.5+k3s1 from the stable channel in November 2026, on a 13.4 image later upgraded to 13.7 and rechecked, with Debian 12.15 tested on its own node.
Plan the K3s cluster on Debian
A K3s cluster has one job split across two roles. The server runs the API, the scheduler and the datastore (SQLite through kine on a single server), and it also runs workloads unless you taint it. Agents dial out to the server on port 6443, and the API server reaches each kubelet back through that tunnel. Metrics-server is the exception: it scrapes every kubelet directly on 10250, so that port stays open between nodes. Pods on different nodes reach each other over flannel’s VXLAN overlay on UDP 8472, so that port has to be open between nodes but never to the internet.

The lab used two Debian 13 VMs on the same subnet: deb-k3s01 at 10.0.1.50 as the server and deb-k3s02 at 10.0.1.51 as the worker. Swap in your own addresses wherever those appear.
Prepare the Debian nodes
The K3s docs set the floor at 2 cores and 2 GB for a server and 1 core with 512 MB for an agent. Treat that as the minimum for a lab, not a production size. On our server the k3s-server process alone held about 810 MiB resident, and the whole node showed 889 MiB in use with Traefik, CoreDNS, metrics-server and two nginx pods running, so a 2 GB server leaves very little room for your own workloads. What each node needs:
- Debian 13 (Trixie) or Debian 12 (Bookworm), amd64 or arm64
- Server: 2 vCPU and 4 GB RAM (tested); worker: 2 vCPU and 2 GB
- A unique hostname per node
- A user with sudo and outbound HTTPS to get.k3s.io and github.com
Hostnames matter more than they look. K3s refuses to register two nodes with the same name, and cloned VMs often keep the template’s hostname. Set it on each node before you install anything:
sudo hostnamectl set-hostname deb-k3s01
Use deb-k3s02 on the worker. Then update the package index and make sure curl is there, since the install script is piped from it.
sudo apt update
sudo apt install -y curl
You do not need to install iptables or nftables first. Our Debian 13 cloud image already had iptables 1.8.11 in nf_tables mode, and K3s used it. On a second node we purged the iptables package completely, and K3s fell back to its own bundled iptables (also 1.8.11, nf_tables, although the K3s known-issues page still lists the bundled build as 1.8.8) and came up Ready all the same.
Open the firewall with ufw
Debian ships with no firewall enabled, so a fresh node accepts everything. If you run ufw, these are the ports K3s needs, taken from the official requirements table and confirmed in the lab.
| Port | Protocol | Between | Purpose |
|---|---|---|---|
| 6443 | TCP | Agents and admins to server | Kubernetes API and K3s supervisor |
| 8472 | UDP | All nodes | Flannel VXLAN pod network |
| 10250 | TCP | All nodes | Kubelet metrics and API (metrics-server) |
| 80, 443 | TCP | Clients to nodes | Traefik ingress |
| 10.42.0.0/16, 10.43.0.0/16 | Any | Pods and services to node | Default pod and service CIDRs |
Run the same rules on both nodes. Allow SSH first, because enabling ufw without it locks you out of a remote box.
sudo apt install -y ufw
sudo ufw allow 22/tcp
sudo ufw allow 6443/tcp
sudo ufw allow 10250/tcp
sudo ufw allow 8472/udp
sudo ufw allow 80,443/tcp
sudo ufw allow from 10.42.0.0/16 to any
sudo ufw allow from 10.43.0.0/16 to any
sudo ufw --force enable
The two CIDR rules are the ones people skip. Without them, pods cannot reach CoreDNS or the API through the service network, and you get timeouts that look like a DNS problem. On a public network, skip the 8472/udp and 10250/tcp lines above and allow those ports from your node addresses only (sudo ufw allow from 10.0.1.51 to any port 8472 proto udp, and the same for 10250 over TCP). If you already ran them, remove them with sudo ufw delete allow 8472/udp and sudo ufw delete allow 10250/tcp first.
Install the K3s server on Debian 13
A config file keeps the install command short and survives upgrades, because the install script never touches it. K3s reads it on every start, so the same file also carries later changes such as secrets encryption. Create the directory and open the file.
sudo mkdir -p /etc/rancher/k3s
sudo vim /etc/rancher/k3s/config.yaml
Add the following. tls-san puts the hostname and IP into the API certificate so kubectl from another machine does not fail with a certificate error, and node-ip pins the address on hosts with more than one interface:
write-kubeconfig-mode: "0600"
tls-san:
- deb-k3s01
- 10.0.1.50
node-ip: 10.0.1.50
Now run the installer. INSTALL_K3S_CHANNEL=stable is already the default, but spelling it out keeps you on stable even if you later paste the command into a script that sets another channel, and it shows anyone reading your notes which channel you chose.
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable sh -
The whole install took 18 seconds on our VM. The script resolves the channel to a release and verifies the binary against its sha256 file before it touches the system. What it leaves behind on the server:
- the
k3sbinary in/usr/local/bin kubectl,crictlandctras symlinks to itk3s-killall.shandk3s-uninstall.sh- the
k3ssystemd unit, enabled and started
You can see each of those steps in the installer output.

“Skipping installation of SELinux RPM” is expected on Debian, which uses AppArmor. Confirm the service is running before you go further.
systemctl status k3s --no-pager | head -12
In the output, look for active (running) and the two ExecStartPre lines that load br_netfilter and overlay with status 0:
● k3s.service - Lightweight Kubernetes
Loaded: loaded (/etc/systemd/system/k3s.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-11-04 04:31:07 UTC; 45s ago
Invocation: 98d8e17810044c619ac9758bf09af74d
Docs: https://k3s.io
Process: 1267 ExecStartPre=/sbin/modprobe br_netfilter (code=exited, status=0/SUCCESS)
Process: 1270 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)
Main PID: 1273 (k3s-server)
Tasks: 81
Memory: 1.5G (peak: 1.5G)
CPU: 30.136s
CGroup: /system.slice/k3s.service
The 1.5G in that output is the cgroup total during first start, page cache included, while it pulls and unpacks images. It settled to about 810 MiB of resident memory for the k3s-server process once everything was up.
Check the iptables backend and kernel modules
On Debian 13, K3s writes every rule through iptables-nft, which means the rules live in the kernel’s nf_tables. Nothing goes to the legacy xtables backend. You can confirm it by counting rules on each side, and the journal tells you how flannel started:
sudo iptables --version
sudo journalctl -u k3s --no-pager | grep -E "nft-expr|Starting flannel"
Then count the rules each backend holds.
echo "nft rules: $(sudo iptables-nft-save 2>/dev/null | grep -c "^-A")"
echo "legacy rules: $(sudo iptables-legacy-save 2>/dev/null | grep -c "^-A")"
180 rules in nf_tables, zero in legacy. Rules split across both backends are a classic cause of pods losing connectivity after someone adds firewall rules with the other tool, so this is the line to check first if that ever happens:

“Starting flannel in iptables mode” is also correct here: flannel drives the iptables command, and that command is the nft variant. If you add rules by hand, use iptables (the nft one) or native nft, never iptables-legacy. After upgrading the server to Debian 13.7 (kernel 6.12.115) and rebooting, the picture was the same: the same warning, flannel in iptables mode, and still zero legacy rules.
Warning: “Failed to load kernel module nft-expr-counter with modprobe”
This warning shows up on every Debian 13 and Debian 12 node we installed, server and agent alike, and it is harmless. K3s asks modprobe for the nft-expr-counter alias, but on Debian’s kernels the counter expression is compiled into nf_tables.ko itself, so there is no separate module to load. We checked by searching the module: nft_counter_do_init, nft_counter_clone and the rest of the counter functions are inside nf_tables.ko.xz, and iptables -L -v shows packet counters ticking on the K3s chains. Leave it alone.
Run k3s check-config
K3s ships its own kernel and userspace checker. Run it once on each new node, because it catches missing cgroup controllers and kernel options before they turn into pods stuck in ContainerCreating:
sudo k3s check-config
The output opens with a modprobe: FATAL: Module configs not found line. That’s the checker trying to load the configs module behind /proc/config.gz; Debian builds its kernels with CONFIG_IKCONFIG unset, so the module does not exist and the checker falls back to /boot/config-* on the next line. What matters is the iptables line, cgroups v2 with the cpu, cpuset and memory controllers, AppArmor, and the final STATUS: pass:

Our nodes ran Debian’s cloud kernel, which ships fewer modules than the generic linux-image-amd64, and every required option was still present, VXLAN and overlayfs included. We did not rerun the check on the generic kernel, but it ships more modules than the cloud build, not fewer.
Set up kubectl for your user
K3s writes its admin kubeconfig to /etc/rancher/k3s/k3s.yaml, readable by root only because of the 0600 mode in the config. Copy it into your home directory instead of loosening that mode, so normal kubectl works without sudo:
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config
echo 'export KUBECONFIG=~/.kube/config' >> ~/.bashrc
source ~/.bashrc
Check that the client and server versions match.
kubectl version
Both sides report the same K3s build, so the kubectl you are running is the K3s symlink and not a separately installed client. If you later install kubectl from another source, keep it within one minor version of the server, because that is the skew the Kubernetes project supports.
Client Version: v1.36.5+k3s1
Kustomize Version: v5.8.1
Server Version: v1.36.5+k3s1
To manage the cluster from your workstation, copy the same file there and change server: https://127.0.0.1:6443 to https://10.0.1.50:6443. The tls-san entries are what make that address valid for the certificate. Our kubectl cheat sheet covers the commands you’ll reach for next.
What the stable channel installs
The packaged components come from the release, not from Debian, so they are identical on Trixie and Bookworm. This lists every image the system pods run.
kubectl get pods -n kube-system -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' | sort -u
On the stable channel in November 2026 that gave these images, with containerd v2.3.4-k3s1.36 underneath and the Traefik chart at 40.1.5+up40.1.0:
rancher/klipper-helm:v0.13.3-build20260727
rancher/klipper-lb:v0.4.17
rancher/local-path-provisioner:v0.0.37
rancher/mirrored-coredns-coredns:1.14.7
rancher/mirrored-library-traefik:3.7.13
rancher/mirrored-metrics-server:v0.9.0
The latest channel pointed at v1.37.1+k3s1 on the same day. Stay on stable for anything you care about; 1.37 adds --write-kubeconfig-name and puts --advertise-address into the API server certificate, but nothing in this guide needs either.
Join a worker node to K3s
Agents authenticate with the server’s node token, so print it on the server first.
sudo cat /var/lib/rancher/k3s/server/node-token
It is a single line starting with K10, 108 characters long in our case. Treat it like a password, because anyone holding it can join a node to your cluster. On the worker, put it in a variable and run the installer with K3S_URL set, which switches the script into agent mode:
export K3S_TOKEN='paste-the-full-node-token-here'
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable K3S_URL=https://10.0.1.50:6443 K3S_TOKEN="${K3S_TOKEN}" sh -
Both installs resolved to v1.36.5+k3s1 because they ran the same day. That is not guaranteed later, since stable moves on its own schedule, and a kubelet newer than its API server is outside the supported version skew. When you add a worker weeks after the server, read the server’s version with k3s --version and replace INSTALL_K3S_CHANNEL=stable with INSTALL_K3S_VERSION=v1.36.5+k3s1 (or whatever the server reports) so the worker can never land on a newer release. The script installs a k3s-agent unit and a separate k3s-agent-uninstall.sh (download and symlink lines trimmed below):
[INFO] Finding release for channel stable
[INFO] Using v1.36.5+k3s1 as release
[INFO] Creating uninstall script /usr/local/bin/k3s-agent-uninstall.sh
[INFO] env: Creating environment file /etc/systemd/system/k3s-agent.service.env
[INFO] systemd: Creating service file /etc/systemd/system/k3s-agent.service
[INFO] systemd: Enabling k3s-agent unit
Created symlink '/etc/systemd/system/multi-user.target.wants/k3s-agent.service' → '/etc/systemd/system/k3s-agent.service'.
[INFO] systemd: Starting k3s-agent
Back on the server, the worker shows up within about 30 seconds. Agents join with no role, so give it a worker label to make kubectl get nodes readable:
kubectl get nodes -o wide
kubectl label node deb-k3s02 node-role.kubernetes.io/worker=worker
kubectl get nodes
Both nodes report Ready on the Debian 13 cloud kernel with containerd 2.3.4:

The agent logs the same nft-expr-counter warning as the server, for the same reason. If a worker stays NotReady, check sudo journalctl -u k3s-agent on it; a wrong token or a blocked 6443 shows up there within seconds.
Deploy a test app behind Traefik
A Deployment with two replicas, a Service and an Ingress is the quickest way to prove scheduling, the overlay network and the ingress all work. Create the manifest:
vim web-demo.yaml
Paste the following manifest into it.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-demo
spec:
replicas: 2
selector:
matchLabels:
app: web-demo
template:
metadata:
labels:
app: web-demo
spec:
containers:
- name: nginx
image: nginx:stable-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: web-demo
spec:
selector:
app: web-demo
ports:
- port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-demo
spec:
ingressClassName: traefik
rules:
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-demo
port:
number: 80
Apply it, wait for the rollout, and request the page with the Host header the Ingress expects.
kubectl apply -f web-demo.yaml
kubectl rollout status deployment/web-demo
kubectl get pods -o wide
kubectl get ingress
curl -s -H "Host: web.example.com" http://10.0.1.50/ | grep -i "<title>"
The scheduler put one replica on each node (10.42.0.x on the server, 10.42.1.x on the worker), and the Ingress picked up both node addresses because K3s’s ServiceLB runs Traefik’s load balancer on every node:

In real use, point a DNS record at the node IPs instead of sending a Host header. Traefik matches routes on the Host header, so a request to the bare IP falls through to its default 404, which is why the curl above sets it explicitly. To see Traefik’s own routing view, follow the guide to enable the Traefik dashboard on K3s.
Verify the cluster through ufw
A firewall that blocks the overlay network fails quietly: pods on one node work and pods on the other do not. So test from both directions with ufw active, using a pod pinned to the worker that resolves a service name and fetches the page across nodes:
for i in 1 2 3 4; do curl -s -o /dev/null -w "%{http_code} " -H "Host: web.example.com" http://10.0.1.51/; curl -s -o /dev/null -w "%{http_code} " -H "Host: web.example.com" http://10.0.1.50/; done; echo
kubectl run dnstest --image=busybox:stable --restart=Never --overrides='{"spec":{"nodeName":"deb-k3s02"}}' -- sh -c "nslookup web-demo.default.svc.cluster.local; wget -qO- http://web-demo | grep -i title"
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/dnstest --timeout=90s
kubectl logs dnstest
kubectl top nodes
Eight requests alternating between both nodes all returned 200, the worker pod resolved the service through CoreDNS at 10.43.0.10, and kubectl top worked, which proves 10250 is open between nodes:

Note the memory column: 889Mi on the server against 240Mi on the worker. That gap is the control plane, and it is why the server needs the bigger VM. The capture above predates the kubectl wait line, which we added after the first run and tested separately; it stops kubectl logs from failing while busybox is still being pulled. Delete the test pod with kubectl delete pod dnstest when you’re done.
Turn on K3s secrets encryption
By default K3s stores Secrets in its datastore unencrypted. On a new cluster the fix is one line, secrets-encryption: true, in config.yaml before the first start. Our cluster was already running, which is the more common case, and since the March 2026 releases (v1.35.3, v1.34.6, v1.33.10) K3s can switch encryption on in place. It takes more steps than the docs make obvious, and the first status check after enable fails with an error that looks worse than it is.
You run the whole sequence on the server, in this order, and each step depends on the one before it because the server only reads the encryption config at startup:
sudo k3s secrets-encrypt enablewrites an encryption config that only has the identity provider- Add
secrets-encryption: trueto/etc/rancher/k3s/config.yamland restart K3s sudo k3s secrets-encrypt rotate-keysgenerates the AES-CBC key and re-encrypts existing Secrets- Restart K3s again and check the status
As commands, that is the following.
sudo k3s secrets-encrypt status
sudo k3s secrets-encrypt enable
echo "secrets-encryption: true" | sudo tee -a /etc/rancher/k3s/config.yaml
sudo systemctl restart k3s
sudo k3s secrets-encrypt status
sudo k3s secrets-encrypt rotate-keys
sudo systemctl restart k3s
sudo k3s secrets-encrypt status
The capture below includes the status check we ran between enable and the restart, so you can see the error. After the first restart the status still reads Disabled at stage start, and that is correct too: only rotate-keys creates a real key. The final status shows Enabled, stage reencrypt_finished and one active AES-CBC key:

Status output is only K3s’s word for it, so we checked the datastore directly. After creating a Secret with a known value, we grepped the SQLite files for it:
kubectl create secret generic demo-secret --from-literal=password=Sup3rS3cret
sudo sh -c "cat /var/lib/rancher/k3s/server/db/state.db*" > /tmp/db.bin
echo "plaintext hits: $(grep -a -c Sup3rS3cret /tmp/db.bin)"
grep -a -o "k8s:enc:aescbc:v1:aescbckey-[0-9T:-]*Z" /tmp/db.bin | sort | uniq -c
rm /tmp/db.bin
Zero plaintext hits, and 18 records carry the AES-CBC envelope prefix with the rotated key’s name:
plaintext hits: 0
18 k8s:enc:aescbc:v1:aescbckey-2026-11-04T04:37:07Z
The release notes for 1.36 and 1.37 list no changes to secrets-encrypt, and the procedure above matches the official secrets-encrypt reference. Back up /var/lib/rancher/k3s/server/cred/encryption-config.json with the rest of the server state, because a datastore restore without that key cannot decrypt anything.
Error: “secret-encrypt error ID 11431”
Running k3s secrets-encrypt status right after enable returns level=fatal msg="Error: see server log for details: Internal error occurred: secret-encrypt error ID 11431". The server journal says why: missing annotation on node deb-k3s01, logged right after Enabling secrets encryption with identity provider, restart with secrets-encryption. The node has not been restarted with the new flag yet, so it has no encryption annotation to report. Add secrets-encryption: true to the config, restart, and the error is gone.
Customize Traefik with a HelmChartConfig
K3s deploys Traefik from a bundled Helm chart, and you change its values with a HelmChartConfig in kube-system rather than editing the deployment. One thing changed under you if you carry old files forward: K3s moved to Traefik chart v40 in 1.35.6, and v40 renamed the ingress-nginx compatibility provider from kubernetesIngressNginx to kubernetesIngressNGINX. Teams migrating off ingress-nginx are exactly the ones who set that key.
Create the override file.
vim traefik-config.yaml
Use the new key name in it.
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
providers:
kubernetesIngressNGINX:
enabled: true
Apply it and check the arguments Traefik actually runs with. The helm-controller re-runs the chart in a new helm-install-traefik pod and then rolls Traefik, so wait until that pod shows Completed and a new Traefik pod appears before you run the rollout check; otherwise it can report the old pod as rolled out.
kubectl apply -f traefik-config.yaml
kubectl -n kube-system get pods | grep -E "helm-install-traefik|^traefik"
kubectl -n kube-system rollout status deploy/traefik
kubectl -n kube-system get deploy traefik -o jsonpath="{.spec.template.spec.containers[0].args}" | tr "," "\n" | grep -i nginx
With the v40 key, the provider and its defaults show up in the container arguments:
"--providers.kubernetesingressnginx"
"--providers.kubernetesingressnginx.controllerClass=k8s.io/ingress-nginx"
"--providers.kubernetesingressnginx.disableSvcExternalName=false"
"--providers.kubernetesingressnginx.httpEntryPoint=web"
"--providers.kubernetesingressnginx.httpsEntryPoint=websecure"
"--providers.kubernetesingressnginx.ingressClass=nginx"
"--providers.kubernetesingressnginx.ingressClassByName=false"
"--providers.kubernetesingressnginx.watchIngressWithoutClass=false"
Then we applied the same file with the old kubernetesIngressNginx spelling. The helm-install job finished as Completed with no relevant error in its log, and the grep returned 0 lines: the provider was simply gone. Any Ingress with ingressClassName: nginx stops routing and nothing tells you why. Grep your HelmChartConfig files for the old spelling before you upgrade an older cluster past 1.35.6.
Run K3s on Debian 12
Debian 12 takes the server install path unchanged. We ran the stable-channel install, the backend check and check-config on a Debian 12.15 node (single node, on its own VM) and compared what K3s saw. Bookworm’s regular security support ended in July 2026 with 12.15 as its final point release, and Debian LTS covers it until June 2028, so plan the move to Trixie. The worker join, ufw, secrets encryption and Traefik steps ran on Debian 13 only, but none of them touches anything Debian-specific:

The differences are all below K3s, and none of them needs a different command.
| Item | Debian 13 (Trixie) | Debian 12 (Bookworm) |
|---|---|---|
| Kernel tested | 6.12.74 (cloud), 6.12.115 after the 13.7 upgrade | 6.1.0-54 (cloud) |
| System iptables | 1.8.11, nf_tables | 1.8.9, nf_tables |
| Rules after first start | 180 nft, 0 legacy (1 node, before the worker joined) | 129 nft, 0 legacy (1 node) |
| nft-expr-counter warning | Yes, harmless | Yes, harmless |
| check-config | pass | pass |
| Traefik helm job restarts on first start | 2 | 2 |
That helm-install-traefik restart count is normal on both: the job retries until the Traefik CRDs from the companion job are registered, then completes. The K3s known-issues page warns about iptables 1.8.0 to 1.8.4, which piled up duplicate rules; Debian 12’s 1.8.9 and Debian 13’s 1.8.11 are both past that range. For other distros, the same flow is covered for K3s on Ubuntu 26.04, Ubuntu 24.04 and Rocky Linux, where SELinux adds its own step.
Remove a node and uninstall K3s
Drain a worker before you remove it, so its pods move to the remaining nodes instead of dying with it. Run both commands on the server.
kubectl drain deb-k3s02 --ignore-daemonsets --delete-emptydir-data
kubectl delete node deb-k3s02
The web-demo replica on the worker was evicted and rescheduled, and the ServiceLB DaemonSet pod was skipped as expected:
node/deb-k3s02 cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/svclb-traefik-cc31be19-h5gtk
evicting pod default/web-demo-6df6bb779d-f6wtn
pod/web-demo-6df6bb779d-f6wtn evicted
node/deb-k3s02 drained
node "deb-k3s02" deleted
Then run the matching uninstall script on the worker. Servers use k3s-uninstall.sh instead.
sudo /usr/local/bin/k3s-agent-uninstall.sh
The script stops the unit, unmounts pod volumes, removes the flannel interface, the binaries and /var/lib/rancher/k3s, leaving an empty /var/lib/rancher. One thing survives on both agents and servers: /etc/rancher/node/password, the node’s join password. Remove /etc/rancher by hand if you want the host truly clean or plan to reuse it under the same name in a different cluster. Your ufw rules stay too, as do ~/.kube/config and the KUBECONFIG line in ~/.bashrc on the server, so clean those up if the box gets a new job.
That is the whole cluster on Debian 13: no extra packages, nftables underneath, and one startup warning plus one check-config complaint that you now know to ignore. If you’re deciding whether K3s is the right lightweight distribution at all, our k0s vs K3s vs MicroK8s comparison measures all three on the same hardware, and the Raspberry Pi K3s cluster build takes the same steps onto arm64 boards.