Status: Draft (2026-10-13), based on official documentation + partial empirical measurements. Structures and default values were verified via official documentation and GitHub repositories accessed on 2026-10-13 (k3s v1.37.1+k3s1, RKE2 v1.37.1+rke2r1, and k0s v1.36.4+k0s.1 were the latest stable releases on that day). Values marked with "Empirically Measured" are the results of direct setup in TT Lab course practice VMs (2026-08~09, versions noted together). Writing Rules: Empirical data is the default. Unmeasured items are marked as "Unmeasured", and items not found in documentation are marked as "Unverified".
Table of Contents
- 1. Why Look at All Three Together
- 2. Origins and Goals
- 3. How the Control Plane Starts
- 4. Storage and HA
- 5. Built-in Components
- 6. Seemingly Similar Yet Different Bands
- References
1. Why Look at All Three Together
k3s, RKE2, and k0s are the names teams encounter first when they try to set up Kubernetes manually. All three make the same promise: "Download a single binary, register it as a service, and a cluster is up." That is why they look similar on the surface.
However, differences emerge once you start operating them. Where you look for logs when a failure occurs, whether you can use etcdctl , whether an ingress is installed by default, and whether the control plane appears in the node list are all different. These differences do not stem from feature lists, but from design and default values. This article compares their designs through diagrams. Security and operations are covered in Part 2, and how to choose is covered in Part 3.
2. Origins and Goals
| k3s | RKE2 | k0s | |
|---|---|---|---|
| Initial Creator | Rancher (now SUSE) | Rancher (SUSE) | Mirantis |
| Other Names | — | RKE Government | — |
| CNCF | Sandbox (Adopted 2020-08-19) | Not a CNCF project (Listed only in certified distributions) | Sandbox (Adopted 2025-01-19) |
| Target Audience | Edge, IoT, ARM boards, CI, development | Security and compliance, especially US federal government sectors | General-purpose distribution eliminating installation and operational friction |
| Latest Stable Release as of 2026-10-13 | v1.37.1+k3s1 (2026-09-30) | v1.37.1+rke2r1 (2026-09-30) | v1.36.4+k0s.1 (2026-09-19) |
Summarizing the characteristics of the three distributions in one line each:
- k3s aims to be "small." Its storage README explains that running multiple components in a single process reduces memory usage, and that in-tree storage drivers and cloud providers were removed from upstream Kubernetes. It notes that its own patches to upstream code are under 1,000 lines, emphasizing that it is a distribution rather than a fork.
- RKE2 combines k3s's installation and operational approach with the old RKE1's principle of being "close to upstream Kubernetes." Its goal is clear: pass CIS benchmarks with almost no manual operator intervention, support FIPS 140-2, and continuously scan for CVEs using Trivy during the build process. RKE1, which relied on Docker, reached its end-of-life on 2025-07-31, and SUSE advises migrating to RKE2 or k3s.
- k0s emphasizes "zero." Its website claims three types of zeros: Zero Friction installation, Zero Deps (single binary with no dependencies other than the host kernel), and Zero Cost under the Apache 2 license. The commonly cited expression "zero overhead" was not found on the official page (unverified).
3. How the Control Plane Starts
The biggest difference is how the control plane components are launched.
k3s: Bundled into a Single Process
k3s server A single binary runs kube-apiserver, scheduler, controller manager, kubelet, kube-proxy, flannel, network policy controller, kine, and helm-controller within the same process . This is the design cited by the README as the primary reason for memory savings, as runtimes and caches that each component would normally maintain separately are used only once.
containerd and runc are bundled with the binary, but executed separately. Looking at the k3s source code (pkg/agent/containerd), k3s launches containerd as a separate process.
In operation, this structure implies the following:
- Logs are gathered in one place. Logs from apiserver and kubelet are mixed together in a single
journalctl -u k3sservice log. crictl psYou cannot find control plane containers using- . That is because they are not containers in the first place.
Trying to fix "
/etc/kubernetes/manifestsstatic pods" learned from kubeadm does not work. Flags are passed via k3s configuration files or arguments like--kube-apiserver-arg.
RKE2: Launched as Static Pods by the Kubelet
rke2 server The RKE2 process does not run the control plane directly; its job is supervision. It launches containerd (and exits if containerd dies), launches the kubelet (and restarts it if it stops), and writes the static pod manifestsfor etcd, kube-apiserver, controller manager, and scheduler into /var/lib/rancher/rke2/agent/pod-manifests/ . The kubelet reads those manifests and launches them as containers, mirroring the shape of kubeadm.
Executables like the kubelet, containerd, and kubectl are included inside the rke2-runtime image and extracted upon initial startup. Add-ons like CNI, CoreDNS, ingress, and metrics-server are installed as Helm charts by the built-in helm-controller.
There is a catch to this structure: systemctl stop rke2-server does not stop static pods. In TT Lab's RKE2 practice (v1.36.4+rke2r1), the etcd container running as root remained even after stopping the service (empirically measured). To completely shut down a node, you use rke2-killall.sh included in the installation artifacts. We will examine what kind of incidents this difference actually causes in Part 2.
k0s: Launched Separately, Controller is Not a Node
k0s launches control plane components not as containers, but as individual independent processes(referred to as "naked" processes in the documentation), with the k0s binary supervising them. etcd, kube-apiserver, scheduler, controller manager, and konnectivity-server start separately. These executables are contained within the k0s binary and extracted to /var/lib/k0s/bin upon first execution.
The most noticeable difference is that neither the kubelet nor the container runtime is present by default on the controller node. Therefore, controllers do not receive workloads and do not appear in kubectl get nodes either. When the API server needs to talk to a worker's kubelet or a pod, it goes through the Konnectivity tunnel (agent port 8132).
There are two flags used when you want to lift this separation:
--enable-worker: The controller also acts as a worker. The FAQ recommends this only for testing, development, and PoC purposes.--single: A single-node mode. It includes--enable-workerbut disables features required for multiple nodes, so you cannot scale up nodes later.
k0s also runs control plane components as a dedicated system user rather than root. The documentation only states up to the point that k0s install creates this system user, and the usernames were confirmed from the source code (pkg/constant/constant.go, v1.36.4+k0s.1 tag). etcd is the etcduser, the API server is the kube-apiserveruser, the scheduler is the kube-scheduleruser, and Konnectivity is the konnectivity-server user. The controller manager's user could not be verified (unverified).
The Three Structures in One Line
| k3s | RKE2 | k0s | |
|---|---|---|---|
| Control Plane Shape | Single process | Static pods launched by kubelet | Separate process per component |
| Container Runtime | containerd and runc bundled | containerd bundled (extracted from runtime image) | containerd and runc bundled |
| Does Server (Controller) Receive Workloads? | Yes | Yes | No by default |
| Control Plane Logs | Single k3s service log | Pod logs (/var/log/pods) |
k0s service logs |
| Add-on Installation Method | Manifest folder + helm-controller | helm-controller | Built-in manifests, Helm extensions optional |
4. Storage and HA
k3s: Starts with SQLite and Can Migrate to etcd
The default storage for a single k3s server is SQLite . Because the API server only knows how to talk to etcd, kine sits in the middle, emulating the etcd API while writing to SQLite in the back. It is lightweight, but you cannot scale servers, nor can you use etcd tools. In TT Lab practice (v1.35.8+k3s1), running k3s etcd-snapshot save in SQLite state was rejected with etcd datastore disabled (empirically measured).
There are two paths to HA:
- Embedded etcd. Launch the first server with
--cluster-initand join the rest with the same token. An odd number of servers, at least 3, is required. A server running on SQLite can also migrate to etcd if restarted with--cluster-init. In practice, the API returned 12 seconds after restart,state.dbwas renamed tostate.db.migrated, and the UIDs of ConfigMaps created before migration remained intact (empirically measured, v1.35.8+k3s1). - External DB. Two or more servers point to the same MySQL, MariaDB, PostgreSQL, or etcd. Versions certified by the documentation are etcd 3.5.21, MySQL 8.0/8.4, MariaDB 10.11/11.4, and PostgreSQL 15.12/16.7/17.3 (doc updated 2026-10-07). Here too, kine emulates the etcd API in front of the SQL DB.
In either case, place a fixed registration address(one of an L4 load balancer, round-robin DNS, or virtual IP) in front of the servers.
RKE2: etcd from the Start
Unless configured otherwise, RKE2 uses embedded etcd . For HA, 3 servers (an odd number) are recommended, and since servers also take workloads by default, 3 servers and 0 agentsis the minimum HA configuration. Nodes join via the fixed registration address on port 9345, and the API uses port 6443.
SQLite can also be used, but the documentation describes it as "experimental" and it is limited to a single server. External SQL DBs can be used via kine (document-verified versions: etcd 3.6.7, MySQL 8.4.8, MariaDB 11.4.10, PostgreSQL 18.3). However, the backup procedure is guided as an "experimental" section.
k0s: etcd for multiple nodes, SQLite for a single node
k0s storage configuration (spec.storage.type) is one of etcd and kine , and the default generated configuration is etcd. The documentation and README feature lists state the default is "etcd for multiple nodes, SQLite for a single node," and mention that MySQL and PostgreSQL can also be placed behind kine.
--single That this defaults to SQLite was verified in practice. When you run --single on a k0s set up with k0s etcd member-list , it gets rejected with wrong storage type: kine (measured). To handle etcd, it must be set up with --enable-worker .
For HA, 3 or 5 controllersare recommended. The documentation points out that 2 nodes are HA from k0s's perspective, but not HA for etcd quorum. In front of the controllers, a TCP load balancer is required to route ports 6443 (API), 8132 (Konnectivity), and 9443 (k0s join API) to all controllers. If you do not have an external load balancer, you can choose between two options:
- CPLB(Control Plane Load Balancing): Controllers share a virtual IP using keepalived.
--singlecannot be used together with this. - NLLB(Node-Local Load Balancing): A small load balancer (Envoy by default) is placed on each worker to distribute traffic to the controllers. This is disabled by default.
5. What Comes Out of the Box
What is already running right after installation is where the three differ the most.
| Component | k3s (v1.37 docs) | RKE2 (v1.36–v1.37 docs) | k0s (v1.36 docs) |
|---|---|---|---|
| CNI | flannel (VXLAN) | canal (flannel VXLAN + Calico policies) | kube-router |
| CNI options | Change flannel backend, or disable and install manually | cilium, calico, flannel, none, multus (secondary) | calico, custom |
| NetworkPolicy | Built-in controller (based on kube-router) | Calico from canal | kube-router |
| Ingress | Traefik v3 | Traefik for new clusters starting from v1.36, ingress-nginx before that | None |
| Service LoadBalancer | ServiceLB (klipper-lb) | ServiceLB exists, but disabled by default | No bundle (explicit statement in docs not verified) |
| StorageClass | local-path | None (measured) | None (directly via CSI) |
| DNS | CoreDNS | CoreDNS (+ autoscaler) | CoreDNS |
| metrics-server | Present | Present | Present |
| Default CIDR (Pods/Services) | 10.42/16 · 10.43/16 | 10.42/16 · 10.43/16 (measured) | 10.244/16 · 10.96/12 |
You need to be careful with RKE2's ingress because the answer varies by version. As upstream support for ingress-nginx ends in March 2026, RKE2 changed the default for new clusters to Traefik starting with v1.36. The service documentation leaves ingress-nginx deprecated in v1.36 and removes it in v1.37, while the v1.37 release notes state that only the migration-ready combination chosen alongside Traefik remains (verified 2026-10-13). Clusters that were already running retain the ingress they were using even after being upgraded. Therefore, the answer to "What is RKE2's ingress?" only comes out if you know the version the cluster was initially created with. In the TT Lab practice VM (v1.36.4+rke2r1), the default ingress class was also traefik, CNI was canal, and the storage class was none (measured).
Conversely, k3s leans towards "pack everything in and make you turn it off." You can pass --disable to traefik, servicelb, local-storage, metrics-server, coredns . A common mistake is deleting Traefik with kubectl delete . Because k3s rewrites and applies the manifests for default components every time it starts, Traefik comes back when restarted. In the practice session, the Traefik Deployment revived within 20 seconds after systemctl restart (measured, v1.35.8+k3s1). To turn it off, you must pass --disable to all servers as well.
k0s leans towards "include nothing and let you choose." The scope section of the README states that it does not bundle ingress, service mesh, or storage, and guides tools like MetalLB, Traefik, and OpenEBS only as examples. This choice is an advantage for clusters that will host real services, because things included by default only become noticeable when something already relies on them, making them harder to strip away at that point.
6. CIDRs Look Similar Yet Different
The last row causes more trouble than you might think. k0s's default CIDRs (pods 10.244/16, services 10.96/12) are values commonly used in standard Kubernetes. The service CIDR is the kubeadm default, and the pod CIDR is the value popularized by flannel examples. Therefore, if you set up k0s inside a VM on top of another Kubernetes cluster using the default values, the inner k0s treats the DNS server address provided by the outer cluster (an address within the outer service CIDR 10.96/12) as its own service IP. This actually happened when creating the TT Lab practice environment (measured). It was a deadlock where DNS died so CNI images couldn't be pulled, and without CNI CoreDNS couldn't start, but the only error visible on screen was image pull timeout.
The 10.42/10.43 ranges in k3s and RKE2 avoid this by coincidence because they are non-standard. Keeping in mind that the experience of "it worked with k3s" was due to coincidence rather than design will help you avoid the same trap when changing distributions. CIDRs are determined when first created, and across all three distributions, they are difficult to change afterwards.
TT's Word. All three look like twins because they are a "single binary," but when you open them up, one is a single body, one uses static pods, and one keeps things completely separate. Since even where to look for logs during a failure differs, make sure to keep a mental picture of each in your head when setting them up for the first time.
References
All documents were accessed on 2026-10-13. Pages on docs.k3s.io and docs.rke2.io showed updates as of 2026-10-07, and the stable version for docs.k0sproject.io was v1.36.4+k0s.1.
- k3s docs: https://docs.k3s.io/, Architecture https://docs.k3s.io/architecture, Datastore https://docs.k3s.io/datastore, Embedded etcd HA https://docs.k3s.io/datastore/ha-embedded, External DB HA https://docs.k3s.io/datastore/ha, Networking https://docs.k3s.io/networking/basic-network-options, Default Services https://docs.k3s.io/networking/networking-services, Packaged Components https://docs.k3s.io/installation/packaged-components
- k3s repository: https://github.com/k3s-io/k3s (README,
pkg/agent/containerd/containerd.go), CNCF Project Page https://www.cncf.io/projects/k3s/ - RKE2 docs: https://docs.rke2.io/, Architecture https://docs.rke2.io/architecture, Embedded etcd https://docs.rke2.io/datastore/embedded, External DB https://docs.rke2.io/datastore/external, HA https://docs.rke2.io/install/ha, Networking https://docs.rke2.io/networking/basic_network_options, Default Services https://docs.rke2.io/networking/networking_services, v1.37 Release Notes https://docs.rke2.io/release-notes/v1.37.X
- RKE1 End of Life Notice (SUSE): https://suse.com/c/rke-end-of-life-by-july-2025-replatform-to-rke2-or-k3s
- k0s docs: https://docs.k0sproject.io/stable/, Architecture https://docs.k0sproject.io/stable/architecture/, Configuration https://docs.k0sproject.io/stable/configuration/, HA https://docs.k0sproject.io/stable/high-availability/, CPLB https://docs.k0sproject.io/stable/cplb/, NLLB https://docs.k0sproject.io/stable/nllb/, Networking https://docs.k0sproject.io/stable/networking/, FAQ https://docs.k0sproject.io/stable/troubleshooting/FAQ/
- k0s repository: https://github.com/k0sproject/k0s (README,
pkg/constant/constant.go), Website https://k0sproject.io/, CNCF Project Page https://www.cncf.io/projects/k0s/ - Measured: TT Lab "Kubernetes Distributions — Setting Them Up Yourself" course practice VM logs (k3s v1.35.8+k3s1, RKE2 v1.36.4+rke2r1, k0s v1.36.4+k0s.0, 2026-09-15, etc.)