Most k3s vs RKE2 comparisons end at "k3s for the edge, RKE2 for compliance", which is true and decides nothing for one server in an office comms room. So we installed k3s, RKE2 and kubeadm at Kubernetes v1.36.5 on three identical Proxmox VMs, 2 vCPUs and 4 GiB of RAM each, then cut the power on every one of them. k3s had its test app answering 25.5 seconds after the kernel booted. kubeadm took 46.4. RKE2 took 75.0.
CloudSpinx builds and runs Kubernetes on client hardware across Kenya as part of our managed Kubernetes service, and which distribution to install is one of the first questions on an on-premises build. Here is what each one used in memory, what it installed without being asked, how it came back from a hard power-off, and what happened when we turned on RKE2's CIS profile on a cluster that was already running.
How much memory do k3s, RKE2 and kubeadm use?
Ten minutes after the first install command, free showed 1,193 MiB in use on the k3s VM, 1,578 MiB on RKE2 and 1,024 MiB on kubeadm. On the k3s and RKE2 VMs, the bare Ubuntu 26.04 image used about 580 MiB before anything was installed.
kubeadm's figure is the smallest, and it also installs the least: etcd, the control plane, kube-proxy, CoreDNS and the Flannel network we applied ourselves. No Ingress controller, no metrics-server, no storage provisioner. k3s folds the API server, scheduler, controller manager, kubelet, kube-proxy and Flannel into one k3s-server process (648 MiB resident), adds Traefik, metrics-server and a local-path provisioner as pods plus a default StorageClass, and still came in 385 MiB under RKE2.
RKE2 runs the upstream layout and then some. etcd and the control plane as static pods, Canal (Calico with Flannel) for the network, a cloud controller manager, a snapshot controller and a CoreDNS autoscaler. That came to 13 running pod sandboxes against 6 on k3s, and its API server alone held 565 MiB, where kubeadm's held 270. It grew the disk by 4.15 GB to k3s's 1.27 GB.
The vendors' own floors set RKE2 apart too. k3s asks for 2 GB on a server, kubeadm for 2 GB per machine, and RKE2's requirements page says 4 GB minimum and recommends 8.
Which one comes back first after a power cut?
k3s, on one timed hard cut each. We stopped each VM the way a dead UPS does, with no shutdown, started it again, and logged seconds from kernel boot until the API passed an authenticated readiness check, until a one-replica nginx app answered on its NodePort, and until every pod had gone Ready again after that boot. k3s and RKE2 were cut at the same moment on the same host; kubeadm was cut on its own later.
- before the API first answered
- API had answered, app not yet
- app had answered, pods not all Ready
The clock starts at kernel boot; firmware and the boot loader come on top and were not measured. k3s and RKE2 were cut together on the same host, kubeadm alone later. A pod only counts once its Ready status changes after this boot, so a status left over from before the cut cannot pass.
Most of RKE2's lag came after its API first answered. That was at 34.3 s, and the app took another 41 seconds. By the time every pod was Ready, RKE2's etcd container showed four restarts where its API server showed one. kubeadm, also on etcd, had everything back by 46.5 s. The k3s figure is that VM's third cut, and its SQLite datastore came back without help from all three.
None of this is high availability. On a three-node cluster the question becomes how long before pods move off a dead node, which is the node failure run we did on k3s. On one machine behind a UPS, a recovery like one of these, plus boot time we did not measure, is added to every outage that outlasts the battery. If that is your setup, the node count and the RAM per node are enough for us to tell you which of the three we would put on it.
What each one installs for you
| k3s | RKE2 | kubeadm | |
|---|---|---|---|
| Datastore | SQLite | embedded etcd | etcd |
| Pod network | Flannel, inside k3s | Canal (Calico + Flannel) | none until you add one |
| Default IngressClass | Traefik | Traefik | none |
| Default StorageClass | local-path | none | none |
| Secrets encrypted at rest | not set | on | not set |
| Pod Security default | privileged | privileged | privileged |
| Leaf certificates | 1 year, renewed at start inside 120 days | 1 year, renewed at start inside 120 days | 1 year, renewed on upgrade |
SQLite is k3s's default and it does not do more than one server. For HA, the k3s docs have you start the first server with --cluster-init and run an odd number, three or more. An existing SQLite server converts by restarting with that flag. We would still start with it on any cluster that might grow, rather than convert one with workloads on it.
Both Rancher distributions now default to Traefik. RKE2 switched for clusters created from v1.36, keeps ingress-nginx only as a deprecated option in that release and removes it for new clusters in v1.37. If you are moving Ingresses off ingress-nginx anyway, what breaks on the way has its own post.
All three issued one-year certificates under ten-year CAs. k3s and RKE2 renew any that are within 120 days of expiry each time they start. kubeadm renews them when you run kubeadm upgrade or kubeadm certs renew, and a kubeadm cluster nobody touches for a year stops taking instructions.
Is RKE2 hardened by default?
Partly. The default install encrypted Secrets at rest, which neither k3s nor kubeadm did. Like k3s, it also switched API server profiling off and refused unauthenticated API calls. Its Pod Security config said enforce: "privileged", though, so the nginx Deployment we ran everywhere, with no securityContext at all, ran on RKE2 too. Restricted pods, an audit log and network policies come from profile: "cis", and that is off until you set it.
We set it on the running RKE2 cluster, following the RKE2 hardening guide: the CIS sysctl file, an etcd user, one line in config.yaml, a restart.
Admission checks a pod when it is created, so running pods survived the switch. A pod that breaks the restricted level, outside the three exempt namespaces, is refused the next time it is created: a rollout (which we saw), a drain or a reschedule.
Then we created the same nginx Deployment again. The API accepted the Deployment with a warning, and its ReplicaSet could not create a single pod:
Error creating: pods "web2-694d4cc785-sxxxz" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
The pod that was already running kept serving, because admission checks a pod when it is created. A kubectl rollout restart of it produced a new ReplicaSet with zero pods. On a real cluster, a workload outside kube-system and the two other exempt namespaces that fails the restricted level keeps running on the day you switch, and is refused the next time its pods are recreated: by a rollout, as here, or by a drain or a reschedule.
Switching a running server had a second cost. The guide asks for the sysctl step "only on fresh installations, before actually using RKE2 to deploy Kubernetes", and we ignored that on purpose. etcd then would not start, though not because of the sysctl file. After the restart every file in its data directory belonged to the new etcd user except member/snap/db, still owned by root, and etcd panicked with permission denied and kept crash-looping, with the API server down alongside it. We watched that for four minutes, then changed the owner of that one file by hand, and rke2-server reported active 7 minutes 46 seconds after the restart. The nginx app answered the one time we checked during the outage, since a running container does not need the control plane. Set the profile when you install, or rehearse the switch on a copy.

Which one we would install
One server, or a few small ones. k3s. It used less memory than RKE2, shipped a default StorageClass where RKE2 shipped none, and came back first. Start it with --cluster-init if a second server is ever likely.
An auditor will hold you to the CIS benchmark. RKE2, with the profile set at install, on nodes with at least the 4 GB minimum it asks for (8 recommended), and with every manifest passing the restricted Pod Security level before the first deploy.
A platform team that wants upstream and its own choices. kubeadm. It installs the control plane and stops, so the network, ingress, storage and the yearly upgrade are all yours.
We did not run Talos Linux here, so it gets no numbers. And if the cluster is EKS, AKS or GKE, none of this is your decision: the provider runs the control plane. A team happy on k3s on one box does not need us to move it to anything else. Where the honest answer is a second machine rather than a different distribution, we hand over a dedicated server in about 24 hours.
Tell us how many nodes you have and how much RAM each one has, whether an auditor names the CIS benchmark, and who will run the cluster once it is built, through the Kubernetes scoping form or on WhatsApp at +254 713 403 044. You get back the distribution we would install, the node sizing, and a plain answer when a managed cloud cluster or no cluster at all is the better buy. Free, and it commits you to nothing.