TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

k3s, RKE2, and k0s Structure Comparison: How the Control Plane Starts and What Comes Built-in

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

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:

3. How the Control Plane Starts

The biggest difference is how the control plane components are launched.

k3s 서버 한 대 · v1.37 문서 기준 k3s server — 한 프로세스 안에서 함께 돈다 kube-apiserver scheduler controller-manager kubelet kube-proxy flannel(CNI) kine → SQLite 네트워크 정책 helm-controller 따로 띄우는 자식 프로세스 containerd(번들) 애드온(파드): CoreDNS · Traefik · ServiceLB · local-path · metrics-server RKE2 서버 한 대 · v1.36~1.37 문서 기준 rke2 server — 감독만 한다 containerd(번들) kubelet kubelet 이 띄우는 정적 파드 etcd kube-apiserver scheduler controller-manager 애드온(헬름 차트): Canal · CoreDNS · Traefik(1.36+) · metrics-server k0s 컨트롤러·워커 분리 · v1.36 문서 기준 k0s controller 각각 따로 뜨는 프로세스 etcd kube-apiserver scheduler controller-manager konnectivity-server kubelet·containerd 없음 k0s worker 다른 노드, 프로세스 containerd(번들) kubelet 파드로 뜨는 것 kube-router(CNI) kube-proxy CoreDNS · metrics-server k0s 는 인그레스·서비스 LB·스토리지를 번들하지 않는다. RKE2 도 ServiceLB 는 기본 꺼짐, 스토리지 클래스는 기본으로 없다.
Figure: Component Structure Comparison of the Three Distributions

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:

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:

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 기본 — 서버 한 대, SQLite k3s server kine(etcd API 흉내) SQLite 서버를 늘릴 수 없다 백업: db 폴더 + 토큰 ② k3s HA — 내장 etcd 또는 외부 DB 고정 등록 주소 :6443 고정 등록 주소 :6443 server etcd server etcd server etcd 홀수 대, 최소 3대 첫 서버에 --cluster-init server server DB 2대 이상 + 외부 DB MySQL · PostgreSQL · etcd ③ RKE2 HA — 서버 3대, etcd 정적 파드 고정 등록 주소 — 등록 :9345 · API :6443 server etcd 파드 server etcd 파드 server etcd 파드 agent agent 홀수 대, 3대 권장 · 서버도 워크로드를 받는다 에이전트는 0대여도 된다 ④ k0s HA — 컨트롤러 3대, 워커는 따로 컨트롤러 etcd 컨트롤러 etcd 컨트롤러 etcd 로드밸런서 또는 CPLB 컨트롤러는 노드 목록에 나오지 않는다(kubelet 없음) 워커 워커 워커 워커는 LB 를 거쳐 붙는다 (노드 로컬 LB 도 있음) 저장소는 기본 etcd, kine(SQLite·MySQL·PostgreSQL)도 고를 수 있다.
Figure: Datastore and HA Topology of the Three Distributions

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:

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:

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.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment