Most “best OS for Kubernetes” lists rank the candidates on feature bullets. I wanted to know how they behave once a cluster is running on them, so I built six single-node clusters on identical VMs and timed them, weighed them and starved them of RAM until they broke.
The six are Talos Linux, Flatcar Container Linux, Fedora CoreOS, Kairos, openSUSE Leap Micro and plain Ubuntu Server as the baseline. Each one got the same test workload and the same four measurements: seconds from power-on until a pod is serving again, memory in use once the node settles, disk used, and the smallest RAM size the node still works on. Along the way I logged what each immutable Kubernetes OS turns on by default (SELinux, automatic updates, automatic reboots), because that’s what bites you on day two. If you’re after the tiny-footprint end of the spectrum instead, the lightweight Linux distros benchmark ran the same method on ten small distros.
Ran all six side by side on my Proxmox box in October 2026, with Kubernetes 1.37 everywhere except Kairos; every number below came off those VMs.
1. Best OS for Kubernetes: the results at a glance
Boot times are medians of three cold boots. “Pod serving” means the test nginx pod is Ready with a container that started after the power-on, which is the moment a real workload is back.
| OS | Kubernetes | Mgmt port / API (s) | Node Ready (s) | Pod serving (s) | Memory used at 300 s | Disk used | RAM floor |
|---|---|---|---|---|---|---|---|
| Talos Linux | upstream, built in | 22.5 / 55.2 | 86.5 | 107.8 | 1,050 MB | 999 MB | 2 GB |
| Flatcar | k3s (sysext) | 25.0 / 26.6 | 34.0 | 37.4 | 1,091 MB | 2,011 MB | 1 GB |
| Fedora CoreOS | k3s | 20.9 / 22.5 | 34.4 | 46.7 | 1,080 MB | 3,626 MB | 1.5 GB |
| Kairos | k3s, built in | 37.5 / 38.9 | 46.1 | 51.9 | 1,105 MB | 2,813 MB | 1.5 GB |
| openSUSE Leap Micro | k3s | 16.0 / 16.4 | 20.8 | 29.0 | 1,123 MB | 4,627 MB | 1.5 GB |
| Ubuntu Server (baseline) | k3s | 16.2 / 16.8 | 23.2 | 28.3 | 1,054 MB | 4,778 MB | 1.5 GB |
Two things stood out. Memory is a wash: every node idled between 1,050 and 1,123 MB, because Kubernetes itself is most of that number and the OS underneath barely registers. Boot time is not. The k3s nodes on Leap Micro and Ubuntu had the pod serving again in under 30 seconds, while Talos took close to two minutes because it starts a full upstream control plane (etcd, kube-apiserver, controller-manager and scheduler as separate static pods) where k3s runs one process on SQLite. Talos still has the smallest disk footprint of the lot, under 1 GB including the cluster.
2. How I tested
Every VM got 2 vCPUs (host CPU type), 4 GB of RAM with the balloon device pinned so it never shrinks, a 20 GB virtio-scsi disk, virtio networking and UEFI firmware without Secure Boot keys, all on the same Proxmox VE node. Leap Micro’s appliance image ships with a 32 GB virtual disk that can’t be shrunk, so its disk is bigger on paper; the disk column counts only the data actually written.
Talos and Kairos run Kubernetes as part of the OS. Flatcar, Fedora CoreOS, Leap Micro and Ubuntu don’t ship it at all, so each of those got the same k3s release, pinned with INSTALL_K3S_VERSION (Flatcar through its k3s system extension). Kairos only publishes k3s one minor release behind, so it ran the newest build it offers. Once each cluster was up, I deployed one nginx Deployment into a bench namespace and paused each OS’s update agent so none of them could reboot itself mid-run. If you are weighing k3s against the other small distributions, I measured k0s, k3s, MicroK8s and kubeadm the same way in k0s vs k3s vs MicroK8s vs kubeadm.
| OS | Build tested | Kernel | Kubernetes | Image and download size |
|---|---|---|---|---|
| Talos Linux | v1.14.2 | 6.18.54-talos | v1.37.1 (built in) | metal-amd64.iso, 362 MB |
| Flatcar Container Linux | 4757.2.1 (stable) | 6.12.111-flatcar | k3s v1.37.1+k3s1 (sysext) | Proxmox VE image 592 MB + 76 MB k3s sysext |
| Fedora CoreOS | 44.20260913.3.2 (stable) | 7.2.5-200.fc44 | k3s v1.37.1+k3s1 | QEMU qcow2.xz, 1,013 MB |
| Kairos | v4.3.0 (Hadron v0.5.1) | 7.1.3-hadron | k3s v1.36.4+k3s1 (built in) | standard ISO, 415 MB |
| openSUSE Leap Micro | 6.2 (Base, Build12.9) | 6.12.0-160000.38 | k3s v1.37.1+k3s1 | Base qcow2, 1,409 MB |
| Ubuntu Server | 26.04.1 LTS | 7.0.0-38-generic | k3s v1.37.1+k3s1 | cloud image, 865 MB |
How each number was taken:
- Boot: seconds from
qm startuntil the management port answers (SSH, or Talos apid on 50000), the API server port 6443 opens, the node reports Ready with a heartbeat newer than the boot, and the test pod is Ready with a freshly started container. Three cold boots, one VM running at a time. - Memory: virtio-balloon stats (total minus available) 300 seconds after the third boot. I checked the method once against the guest itself: the balloon read 1,040 MB used on Ubuntu while
/proc/meminfogave 1,079 MB a few seconds later, with identical totals. - Disk: the ZFS
logicalreferencedsize of the VM disk after the benchmark, so it’s data written, not the virtual disk size. - RAM floor: boot at a smaller size and pass only if the node goes Ready and the test pod is serving within 420 seconds, and both are still true 60 seconds later. Each edge size was tried three times. Talos and Flatcar were then rerun three times each from a clean ZFS snapshot, and the floor table uses those runs, because my first Talos node was damaged by its low-memory boots (more on that below). In Flatcar’s earlier 1 GB series, one of three runs served the pod at 80 seconds and lost it a minute later.
3. Talos Linux: Kubernetes is the whole OS
Talos has no shell, no SSH and no package manager. You drive it through an API with talosctl, and the machine config you apply is the entire state of the box. On my node the whole OS was twelve services (apid, containerd, etcd, kubelet, machined, trustd and a handful of helpers), against 16 to 21 running systemd services on the other five.
The ISO boots into maintenance mode and waits. From the admin machine, this is the sequence I ran for a single node that also schedules workloads. After apply-config the node installs itself to disk and reboots, so wait for the API to answer before bootstrapping:
IP=192.168.1.50
talosctl gen config hl-talos https://$IP:6443 --install-disk /dev/sda --config-patch @talos-patch.yaml --output-dir . --force
talosctl apply-config --insecure --nodes $IP --file controlplane.yaml
export TALOSCONFIG=$PWD/talosconfig
talosctl config endpoint $IP
talosctl config node $IP
until talosctl version >/dev/null 2>&1; do sleep 5; done
talosctl bootstrap
talosctl kubeconfig ./kubeconfig --force
kubectl --kubeconfig ./kubeconfig get nodes -o wide
The patch file is where I lost ten minutes. Most guides tell you to set cluster.allowSchedulingOnControlPlanes: true, and on Talos 1.14 that gets rejected at apply time:
error applying configuration: rpc error: code = InvalidArgument desc = 1 error occurred:
* .cluster.allowSchedulingOnControlPlanes is already set in v1alpha1 config
The control-plane taint now lives in a separate KubeNodeConfig document. Delete the taint there and leave the old field alone. This is the whole talos-patch.yaml that worked:
apiVersion: v1alpha1
kind: KubeNodeConfig
taints:
$patch: delete
Talos is also the only one that complained about my deliberately lazy nginx Deployment. It runs the Pod Security admission controller with warnings at the restricted level, so kubectl create deployment printed “would violate PodSecurity “restricted:latest”: allowPrivilegeEscalation != false” and created the pod anyway. The five k3s nodes said nothing.
Talos also needs the most memory of the six, and the reason isn’t what I expected. Its RAM floor runs started from the same clean ZFS snapshot every time, so one bad boot couldn’t poison the next. At 2 GB the node went Ready at about 108 seconds and served the pod at about 122, three times out of three. At 1.5 GB it never went Ready in three tries, yet talosctl memory still showed about 580 MB available. The kubelet log explains it:
talosctl -n $IP logs kubelet | grep -iE "preempt|evict|admit"
Buried in the output is the line where the kubelet refuses to start the scheduler, one of the control-plane pods Talos itself defines:
192.168.1.50: {"ts":1791105772967.4219,"caller":"allocation/allocation_manager.go:660","msg":"Pod admission denied","v":0,"podUID":"cc10153ede0e443224e0f13797833662","pod":{"name":"kube-scheduler-talos-h73-ett","namespace":"kube-system"},"reason":"UnexpectedAdmissionError","message":"Unexpected error while attempting to recover from admission failure: preemption: error finding a set of pods to preempt: no set of running pods found to reclaim resources: [(res: memory, q: 17768448), ]","operation":"add"}
That’s an accounting failure, not a memory shortage. Talos sets the kubelet’s systemReserved memory to 512 MiB and keeps the default 100 MiB hard-eviction threshold, so a 1.5 GB VM leaves only 815 MiB allocatable to pods. The three control-plane static pods request 832 MiB between them: 512 MiB for the API server, 256 MiB for the controller manager and 64 MiB for the scheduler. The 17 MiB in the error (17,768,448 bytes) is that gap, and in these boots the scheduler was the pod left out. With no scheduler the node keeps its not-ready taint, so nothing else ever lands. Six minutes into one of those boots, the Talos console dashboard showed exactly that state:

Updates are A/B: talosctl upgrade writes the new image to the inactive slot and Talos rolls back if it doesn’t boot. Nothing upgrades on its own. The Talos install guide covers a multi-node cluster if you’re past the single-node stage.
4. Flatcar Container Linux: k3s as a system extension
Flatcar keeps /usr read-only and ships no package manager, so Kubernetes arrives as a systemd-sysext image from the Flatcar sysext bakery. Ignition downloads the k3s extension on first boot and a symlink enables the server unit. On Proxmox, Flatcar reads the Ignition JSON from the cloud-init drive, so you write Butane, convert it, and point --cicustom at the result. I did all of it as root on the Proxmox host, which doesn’t ship butane; I used the static binary from the Butane releases page. Create the Butane file:
vim flatcar-k3s.bu
This is the file the Flatcar node was built from, with my public key swapped for a placeholder:
variant: flatcar
version: 1.0.0
passwd:
users:
- name: kibet
groups: [sudo, docker]
ssh_authorized_keys:
- ssh-ed25519 AAAA...your-public-key
storage:
files:
- path: /etc/hostname
contents:
inline: hl-flatcar
- path: /opt/extensions/k3s/k3s-v1.37.1+k3s1-x86-64.raw
mode: 0644
contents:
source: https://extensions.flatcar.org/extensions/k3s-v1.37.1+k3s1-x86-64.raw
- path: /etc/sysupdate.k3s-v1.37.d/k3s-v1.37.conf
contents:
source: https://extensions.flatcar.org/extensions/k3s/k3s-v1.37.conf
- path: /etc/sysupdate.d/noop.conf
contents:
source: https://extensions.flatcar.org/extensions/noop.conf
links:
- target: /opt/extensions/k3s/k3s-v1.37.1+k3s1-x86-64.raw
path: /etc/extensions/k3s.raw
hard: false
- path: /etc/systemd/system/multi-user.target.wants/k3s.service
target: /usr/local/lib/systemd/system/k3s.service
overwrite: true
systemd:
units:
- name: systemd-sysupdate.timer
enabled: true
- name: systemd-sysupdate.service
dropins:
- name: k3s.conf
contents: |
[Service]
ExecStartPre=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/k3s.raw > /tmp/k3s"
ExecStartPre=/usr/lib/systemd/systemd-sysupdate -C k3s-v1.37 update
ExecStartPost=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/k3s.raw > /tmp/k3s-new"
ExecStartPost=/usr/bin/sh -c "if ! cmp --silent /tmp/k3s /tmp/k3s-new; then touch /run/reboot-required; fi"
The Ignition file goes into the snippets directory of the local storage, which a fresh Proxmox install doesn’t allow. Mine already did; on a default host add snippets to the content types first (keep anything else your local entry in /etc/pve/storage.cfg already lists):
pvesm set local --content iso,vztmpl,backup,snippets
Then convert the Butane file straight into that directory and attach it through a cloud-init drive (zfs-pool is my VM storage, use yours):
butane --strict --files-dir . flatcar-k3s.bu > /var/lib/vz/snippets/flatcar.ign
qm set 9702 --ide2 zfs-pool:cloudinit --cicustom user=local:snippets/flatcar.ign
The node came up with k3s merged into /usr and no install step at all. systemd-sysext status on the node lists it next to the Docker and containerd extensions Flatcar ships:
HIERARCHY EXTENSIONS SINCE
/opt none -
/usr containerd-flatcar Sun 2026-10-04 09:41:40 UTC
docker-flatcar
k3s
oem-proxmoxve
Two defaults are worth knowing. SELinux is compiled in but runs Permissive, and update-engine plus locksmithd run from the first boot, so a Flatcar node downloads new releases into its spare /usr partition and reboots into them unless you configure otherwise. The k3s extension updates itself within its minor release through systemd-sysupdate, but only on disk: the unit swaps the symlink and touches /run/reboot-required, and the new k3s isn’t merged until the node reboots, which you trigger yourself or hand to something like kured. It was also the only k3s node that kept the pod serving on 1 GB, three times out of three from a clean snapshot, with as little as 27 MB left available. At 768 MB it fell over: the console filled with journald complaining about memory pressure and SSH stopped answering.

For a full kubeadm cluster on Flatcar instead of k3s, there’s a Flatcar Kubernetes on KVM walkthrough.
5. Fedora CoreOS: Ignition plus the k3s installer
Fedora CoreOS takes Ignition too, and on plain QEMU or Proxmox the simplest path is passing it through fw_cfg. The Butane file is short because k3s goes on afterwards:
vim fcos.bu
Mind the group name in it:
variant: fcos
version: 1.6.0
passwd:
users:
- name: kibet
groups: [sudo]
ssh_authorized_keys:
- ssh-ed25519 AAAA...your-public-key
storage:
files:
- path: /etc/hostname
mode: 0644
contents:
inline: hl-fcos
Convert it and hand the Ignition file to the VM through QEMU’s firmware config device. Proxmox has no field for this, so it goes in --args:
butane --strict fcos.bu > fcos.ign
qm set 9703 --args "-fw_cfg name=opt/com.coreos/config,file=$PWD/fcos.ign"
My first build used groups: [wheel] out of RHEL habit, and the k3s installer died at the first sudo with “sudo: a terminal is required to read the password”. Fedora CoreOS grants passwordless sudo to the sudo group, not wheel, and since Ignition only runs on first boot I had to re-provision the VM. Once the user was right, the standard installer handled the immutable bits itself:
curl -sfL https://get.k3s.io | sudo INSTALL_K3S_VERSION=v1.37.1+k3s1 sh -
It spotted rpm-ostree, layered the k3s-selinux policy package and asked for a reboot before k3s would run properly:
Added:
k3s-selinux-1.6-1.coreos.noarch
Changes queued for next boot. Run "systemctl reboot" to start a reboot
[WARN] Please reboot your machine to activate the changes and avoid data loss.
Fedora CoreOS and Leap Micro both run SELinux Enforcing out of the box, and on each the k3s installer layered in the k3s-selinux policy with nothing extra configured. Fedora CoreOS also has the most aggressive update default: Zincati is enabled with the immediate strategy, which finalizes a new release and reboots the node as soon as it lands. On a single-node cluster that’s an unplanned outage, so switch to the periodic strategy with a maintenance window or use FleetLock before you put anything real on it. Its 3.6 GB disk figure is the second-largest of the immutable group. The older Fedora CoreOS on KVM guide covers the libvirt side of provisioning.
6. Kairos: k3s baked into the image
Kairos changed shape this year. From the v4 releases the project only publishes images built on Hadron, its own minimal distro; the Ubuntu and openSUSE flavours are now something you build yourself. The standard ISO carries k3s, and a cloud-config on a second CD (volume label cidata) installs the whole thing unattended:
vim user-data
Set the target disk and turn k3s on:
#cloud-config
hostname: hl-kairos
install:
device: /dev/sda
auto: true
reboot: true
users:
- name: kibet
groups: [admin]
ssh_authorized_keys:
- ssh-ed25519 AAAA...your-public-key
k3s:
enabled: true
The Kairos download is called kairos-hadron-v0.5.1-standard-amd64-generic-v4.3.0-k3sv1.36.4+k3s1.iso; I copied it into the ISO store as kairos-v4.3.0-k3s.iso to keep the commands short. Pack the user data with a one-line meta-data file into a seed ISO in the Proxmox ISO store, attach both ISOs and boot from the empty disk first so the installed system wins after the reboot:
echo "instance-id: hl-kairos" > meta-data
genisoimage -quiet -o /var/lib/vz/template/iso/kairos-cidata.iso -volid cidata -joliet -rock user-data meta-data
qm set 9704 --ide2 local:iso/kairos-v4.3.0-k3s.iso,media=cdrom --ide3 local:iso/kairos-cidata.iso,media=cdrom
qm set 9704 --boot "order=scsi0;ide2"
Check the order with qm config 9704 | grep boot before the first start. My first attempt ended up with order=net0;ide2;ide3 and sat at a UEFI PXE prompt; setting it again on its own fixed it.
Nothing else happened by hand. The installer partitioned the disk, rebooted and came back with this boot menu, which is the clearest picture of how Kairos updates: an active and a passive image for A/B upgrades, a recovery system and a state reset entry.

Kairos was the slowest of the k3s nodes to accept SSH (37.5 seconds median) and the slowest k3s node overall to get the pod serving. There’s no SELinux in its kernel, the root filesystem is a 625 MB read-only image, and no update agent runs: upgrades happen when you run kairos-agent upgrade or roll them out from a cluster. The one real catch is version lag. While the other k3s nodes ran 1.37, Kairos only offered a k3s 1.36 image.
7. openSUSE Leap Micro: the fastest immutable node
Leap Micro is SUSE’s community build of SL Micro: a read-only btrfs root, transactional-update for changes and snapshots to roll back to. Use the Base appliance image. The Default one ships libvirt and a Cockpit prompt you don’t need on a Kubernetes node. First-boot setup is a Combustion shell script passed through fw_cfg:
vim combustion.sh
The mount /home line is there because I got burned without it:
#!/bin/bash
# combustion: network
# /home is a separate btrfs subvolume, not mounted while combustion runs
mount /home
echo hl-leapmicro > /etc/hostname
useradd -m -G wheel kibet
mkdir -p /home/kibet/.ssh
echo 'ssh-ed25519 AAAA...your-public-key' > /home/kibet/.ssh/authorized_keys
chown -R kibet: /home/kibet/.ssh
chmod 700 /home/kibet/.ssh; chmod 600 /home/kibet/.ssh/authorized_keys
echo 'kibet ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/kibet
systemctl enable sshd
The script reaches the VM the same way Fedora CoreOS gets Ignition:
qm set 9705 --args "-fw_cfg name=opt/org.opensuse.combustion/script,file=$PWD/combustion.sh"
Without the mount /home line, Combustion writes the key into the root subvolume underneath the real /home. The hostname gets set, so it looks like it worked, and then SSH answers “Permission denied (publickey,password,keyboard-interactive)”. With the user in place, the same k3s installer detected transactional-update, installed the SELinux policy into a new snapshot and needed one reboot.
After that reboot, Leap Micro was the fastest immutable OS in the test: SSH at 16 seconds and the pod serving at 29. SELinux runs Enforcing. Updates are on by default: transactional-update.timer applies them daily into a new snapshot and rebootmgr reboots into it, so plan a maintenance window here too. It also wrote the most data of the immutable group (4.6 GB). There’s a separate guide for k3s on regular openSUSE Leap if you’d rather have a mutable SUSE base.
8. Ubuntu Server: the baseline that isn’t immutable
I added Ubuntu Server to answer a fair question: does immutability cost anything? Measured against the stock cloud image with cloud-init and the same k3s, the answer is no. Ubuntu had the pod serving in 28.3 seconds, a hair ahead of Leap Micro, at 1,054 MB of memory. It wrote the most data (4.8 GB), and its /usr is writable by root. That’s the real difference: anything can change the node underneath Kubernetes, and nothing rolls it back. The k3s on Ubuntu 26.04 guide covers this setup in detail.
Two things caught me. The cloud image changed IP after its first reboot (one address from cloud-init’s first-boot lease, a different one on every boot after), which broke my first benchmark run until I noticed. Pin a DHCP reservation or a static address for any Kubernetes node. Second, the node came back from its first reboot on a newer kernel than it was installed with. That wasn’t Ubuntu’s doing: the cloud-init user-data Proxmox generates sets package_upgrade: true by default, so cloud-init upgraded every package three minutes into the first boot. Ubuntu’s own unattended-upgrades is enabled for security updates but never reboots by itself, because Unattended-Upgrade::Automatic-Reboot is commented out by default.
9. What fails first when a Kubernetes node runs out of RAM
None of the failures were OOM kills. I grepped the kernel log of every failed boot I could still log into for “out of memory” and “killed process” and found nothing. The k3s nodes slowed until Kubernetes’ own health checks gave up, and most of them even kept reporting Ready while the test pod couldn’t come back. From a clean snapshot, Talos never got as far as Ready, for the bookkeeping reason in section 3.
| OS | Last size that passed | First size that failed | What the failure looked like |
|---|---|---|---|
| Talos Linux | 2048 MB (3 of 3, clean snapshot) | 1536 MB (0 of 3, clean snapshot) | Kubelet refused to admit kube-scheduler (UnexpectedAdmissionError): control-plane requests of 832 MiB exceed the 815 MiB left after Talos reserves 512 MiB; node never Ready |
| Flatcar | 1024 MB (3 of 3, clean snapshot) | 768 MB (0 of 3) | Console flooded with “systemd-journald: Under memory pressure, flushing caches”; SSH stopped answering |
| Fedora CoreOS | 1536 MB (3 of 3) | 1024 MB (0 of 3) | Node Ready, test pod never came back; 120 to 140 MB still available |
| Kairos | 1536 MB (3 of 3) | 1024 MB (0 of 3) | Twice no pod at all; once the pod served at 123 s and was gone 60 s later |
| openSUSE Leap Micro | 1536 MB (3 of 3) | 1024 MB (0 of 3) | Node Ready, pod never came back; SSH timed out during banner exchange |
| Ubuntu Server | 1536 MB (3 of 3) | 1024 MB (0 of 3) | “PLEG is not healthy”, kine “Slow SQL”, API calls timing out |
On the k3s nodes the symptom at 1 GB was thrashing, not killing. With no swap, the kernel kept evicting the page cache that k3s needs to run, and the journal on the Ubuntu node at 1024 MB shows the kubelet and the API server both stalling:
Oct 04 07:53:02 hl-ubuntu k3s[1112]: E1004 07:53:02.308067 1112 kubelet.go:2860] "Housekeeping took longer than expected" err="housekeeping took too long" expected="1s" actual="51.347s"
Oct 04 07:53:02 hl-ubuntu k3s[1112]: E1004 07:53:02.309380 1112 kubelet.go:2697] "Skipping pod synchronization" err="PLEG is not healthy: pleg was last seen active 3m34.973254551s ago; threshold is 3m0s"
Oct 04 07:54:01 hl-ubuntu k3s[1112]: time="2026-10-04T07:54:01Z" level=warning msg="Slow SQL: SELECT current_rev, compact_rev, id, name, created, deleted, create_revision, prev_revision, lease, value FROM (SELECT MAX(id) AS current_rev FROM kine) AS current_rev, ...
That last line is kine, the SQLite shim k3s uses in place of etcd, taking seconds per query. kubectl from outside just timed out with “Unable to connect to the server: context deadline exceeded” while free inside the guest still showed about 200 MB available. On Talos the failure was different: from the clean snapshot the kubelet never admitted kube-scheduler (section 3), so the node never lost its not-ready taint even with memory to spare.
One more lesson, and it matters if you test this yourself: low-memory boots can damage a cluster. My first Talos node actually reached Ready at 1.5 GB on some early boots, but its kubelet then evicted kube-controller-manager with “Preempted in order to admit critical pod”. After a few of those boots it stayed broken even at 4 GB, with kube-controller-manager stuck in ContainerStatusUnknown and the test pod Pending. I rebuilt it and ran every later Talos attempt from a ZFS snapshot of a healthy node. Treat a node that has been memory-starved as suspect until you’ve checked kubectl get pods -A.
10. Kubernetes OSes I left out, and why
Several names still turn up on Kubernetes OS lists, but they’re dead, locked to one platform or not a standalone OS. I checked each one’s release history before dropping it:
- RancherOS and k3OS: both GitHub repositories are archived. RancherOS last shipped in February 2021 and k3OS in October 2021.
- CoreOS Container Linux: end of life since May 2020. Flatcar is its direct continuation and Fedora CoreOS its spiritual one.
- Red Hat Enterprise Linux CoreOS: Red Hat supports it only as part of OpenShift. If that’s where you’re headed, the OpenShift on KVM guide installs it for you.
- Bottlerocket: still active, but its Kubernetes variants are built only for AWS and VMware. The bare-metal Kubernetes variants were dropped in 2024, and the remaining
metal-devbuild doesn’t join a cluster. - Photon OS: the latest GA release is still 5.0 from April 2023, and it’s a general container host with no Kubernetes of its own.
- Ubuntu Core: Canonical’s Kubernetes snap is classic-confined, so only strict MicroK8s runs on it, and a signed image wants a store account at first boot.
- Elemental: needs Rancher Manager and its operator to provision anything. It’s a fleet tool, not an OS you install on one box.
11. Which one I’d run, and what I’d change for production
For a cluster I care about, I’d run Talos. It boots the slowest and has the steepest learning curve, but it’s the smallest footprint here (999 MB on disk with the cluster), it has no shell to drift, and the only way to change a node is a config you can keep in Git. Give control-plane nodes at least 4 GB in practice. Talos documents 2 GiB as the minimum, and that’s exactly where my tests put the edge.
For k3s at the edge or in a homelab, Leap Micro and Flatcar are the picks. Leap Micro was the fastest immutable node and ships SELinux Enforcing, and Flatcar runs k3s on the least RAM of anything here (1 GB), as a system extension with a k3s that stages its own updates within its minor release and applies them on reboot. Fedora CoreOS is fine too, but only after you change Zincati’s update strategy. Kairos makes sense when you want the A/B image and recovery partition with k3s already in the image and you can live with one minor version of lag.
Whichever you pick, three changes apply to all of them before production:
- Control the reboots. Flatcar, Fedora CoreOS and Leap Micro all reboot themselves for updates by default. Give them a maintenance window or a lock (FleetLock, locksmith, rebootmgr) so two nodes never restart at once.
- Size by measurement, not by the minimum. Every node here idled at about 1.1 GB before any real workload, and the k3s floor was 1 to 1.5 GB with one nginx pod. Start at 4 GB per node and watch the actual usage.
- Pin node addresses. Ubuntu changing IP after one reboot broke my kubeconfig, and a moving control-plane address breaks a real cluster’s certificates the same way.
Storage is the next decision after the OS. The k3s and RKE2 storage guide covers local-path versus a real CSI driver, and MetalLB gives a bare-metal cluster working LoadBalancer services.
Missing Talos Linux, probably the most secure and robust Kubernetes Linux
Ubuntu is easy to use and the support on the internet is overwhelming. This doesn’t mean that other Linux distros don’t have competitive features and capabilities. I read a review on the best Linux distro for docker. I do agree with the writer’s conclusion.
Thanks for your feedback.
You need to add Talos Linux to the review!
https://talos.dev/
Many thanks for the article, very intersting
Article should note that RancherOS is no longer supported though not deprecated.
Would be nice to have any pointers to running above OS’s on WSL