How To Install Kubernetes on Fedora 44

Install Kubernetes on Fedora 44

Most Kubernetes tutorials assume a clean Ubuntu VM and a lot of luck. Fedora is a different animal. It ships a very new kernel, cgroup v2 by default, SELinux in enforcing mode, firewalld, and swap on zram that quietly reappears after a reboot. Each of those can break a kubeadm bootstrap if you follow a generic guide line by line. The result is a kubelet that crash-loops, a control plane that never comes up, or pods that cannot reach each other.

Fedora 44 was released in late April 2026. It tracks recent kernels, and its security advisories have already covered the 7.0.x and 7.1.x series. That makes it a good fit for a lab, a homelab cluster, or a staging environment where you want modern kernel features such as better cgroup v2 accounting. It is rarely the right choice for a long-lived production control plane, because the release cycle is short and you will be upgrading the OS roughly every year. If you accept that trade-off, the platform is pleasant to work with.

This guide builds a single control-plane cluster with one or more workers using kubeadm, CRI-O as the container runtime, and the official community package repositories. Along the way it explains why each step exists, because understanding the reason is what lets you fix things at 2 AM when a node goes NotReady. A short troubleshooting section, security notes and tuning advice follow the main procedure.

What You Need Before Starting

Plan the hardware and network first. Changing hostnames or IP addresses after bootstrap is painful.

Minimum per node:

  • 2 vCPUs (4 is more comfortable for a control plane)
  • 2 GB RAM for workers, 4 GB for the control plane in practice
  • 20 GB of disk, SSD or NVMe preferred, since etcd is sensitive to fsync latency
  • A static IP or a DHCP reservation
  • Unique hostname, MAC address and product_uuid on every node
  • Fedora 44 Server or Workstation, fully updated

Example lab layout used in this guide:

Role Hostname IP address
Control plane k8s-cp1 192.168.50.10
Worker k8s-w1 192.168.50.11
Worker k8s-w2 192.168.50.12

Cloned VMs are the classic trap. If you cloned a template, the machine-id and product_uuid may be identical across nodes, and the cluster will behave strangely. Check before you proceed:

cat /etc/machine-id
sudo cat /sys/class/dmi/id/product_uuid
ip link

If two nodes match, regenerate the machine-id on the clone:

sudo rm -f /etc/machine-id
sudo systemd-machine-id-setup

Step 1: Update the System and Set Hostnames

Start from a current baseline. A kernel update mid-install is annoying, so get it out of the way now.

sudo dnf upgrade --refresh -y
sudo reboot

Fedora’s own guidance is that dnf update --security covers security-relevant updates including the kernel, but for a fresh node a full upgrade is cleaner.

Set the hostname on each machine:

sudo hostnamectl set-hostname k8s-cp1

Then add all nodes to /etc/hosts on every machine. Relying on DNS you do not control is a common cause of mysterious join failures.

sudo tee -a /etc/hosts <<'EOF'
192.168.50.10 k8s-cp1
192.168.50.11 k8s-w1
192.168.50.12 k8s-w2
EOF

Time sync matters too. Certificates, etcd leader elections and token validity all depend on sane clocks. Fedora ships chrony, so confirm it is running:

sudo systemctl enable --now chronyd
chronyc tracking

Step 2: Disable Swap, Including zram

The kubelet has historically refused to start when swap is active. Newer Kubernetes releases do have a swap feature, but it is a deliberate opt-in with its own configuration, and a default install should still run swap-free so that memory limits and eviction behave predictably.

Here is the Fedora-specific catch. Fedora enables swap on zram out of the box through zram-generator. Running swapoff -a works until the next reboot, then the zram device returns and the kubelet fails. Check what you have:

swapon --show
zramctl

The durable fix is to remove the default zram configuration package and reboot:

sudo dnf remove -y zram-generator-defaults
sudo swapoff -a
sudo reboot

If the system also has a traditional swap partition or file, comment it out in /etc/fstab:

sudo sed -i.bak '/\sswap\s/ s/^/#/' /etc/fstab

After the reboot, swapon --show should print nothing. Verify before moving on, because this single issue accounts for a big share of “kubelet will not start” reports on Fedora.

Step 3: Load Kernel Modules and Set sysctl Parameters

Pod networking relies on bridged traffic being visible to iptables or nftables, and on IP forwarding being on. Two modules are needed: overlay for container filesystems and br_netfilter for bridge filtering.

sudo tee /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

Now the sysctl values:

sudo tee /etc/sysctl.d/99-kubernetes.conf <<'EOF'
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sudo sysctl --system

Confirm they stuck:

lsmod | grep -E 'overlay|br_netfilter'
sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables

A small observation from running clusters: if ip_forward is 1 at the console but 0 after reboot, something else is overriding it. NetworkManager profiles and leftover files in /etc/sysctl.conf are the usual suspects. Run sudo sysctl --system and read the order in which files are applied.

Step 4: Handle SELinux Properly

Many guides tell you to set SELinux to permissive and move on. That works, but it throws away one of Fedora’s best security features. The kubeadm documentation itself has traditionally recommended permissive mode so containers can access host filesystems such as pod networks, and the Kubernetes packages ship with SELinux support that has matured considerably.

Here is a pragmatic approach. For a lab, use permissive mode and get a working cluster first:

sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

For anything serious, keep enforcing and install container-selinux, which CRI-O’s own instructions list as a required dependency. Watch the audit log for denials when you add storage drivers or hostPath-heavy workloads:

sudo ausearch -m avc -ts recent

The honest advice: bootstrap in permissive, confirm the cluster is healthy, then switch to enforcing and test your actual workloads. If something breaks, you know it is SELinux and not your configuration.

Step 5: Configure the Firewall

Fedora Server and Workstation both run firewalld. Blocking the wrong port gives you symptoms that look like networking bugs: joins that hang, kubectl logs timing out, metrics that never arrive.

Control plane ports:

sudo firewall-cmd --permanent --add-port=6443/tcp
sudo firewall-cmd --permanent --add-port=2379-2380/tcp
sudo firewall-cmd --permanent --add-port=10250/tcp
sudo firewall-cmd --permanent --add-port=10257/tcp
sudo firewall-cmd --permanent --add-port=10259/tcp

Worker node ports:

sudo firewall-cmd --permanent --add-port=10250/tcp
sudo firewall-cmd --permanent --add-port=10256/tcp
sudo firewall-cmd --permanent --add-port=30000-32767/tcp

Flannel’s VXLAN overlay (all nodes):

sudo firewall-cmd --permanent --add-port=8472/udp
sudo firewall-cmd --reload

Port 6443 is the API server, 2379 to 2380 is etcd, and 10250 is the kubelet API. NodePort services use the 30000 to 32767 range. If you pick a different CNI later, its required ports will differ, so check its documentation. Do not expose etcd ports to the wider network. Restrict them with a rich rule or a dedicated zone if your control plane has more than one interface.

Step 6: Add the Kubernetes and CRI-O Repositories

Kubernetes packages come from pkgs.k8s.io, and each minor version has its own repository. The old Google-hosted yum.kubernetes.io repositories were deprecated and frozen in September 2023, and versions released after that date are only available from the new location. If you find an old tutorial pointing at the legacy URL, close the tab.

At the time of writing, the current documentation targets Kubernetes v1.37. Set a variable so you can change it in one place:

export K8S_VERSION=v1.37
export CRIO_VERSION=v1.37

Create the Kubernetes repo. The exclude line is intentional: it stops a routine dnf upgrade from bumping your cluster components to a new minor version unannounced, which could break the control plane.

sudo tee /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/${K8S_VERSION}/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/${K8S_VERSION}/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF

Add the CRI-O repo. The CRI-O project publishes through community infrastructure and recommends matching its minor version to your Kubernetes minor version:

sudo tee /etc/yum.repos.d/cri-o.repo <<EOF
[cri-o]
name=CRI-O
baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/${CRIO_VERSION}/rpm/
enabled=1
gpgcheck=1
gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/${CRIO_VERSION}/rpm/repodata/repomd.xml.key
EOF

CRI-O’s repository location has moved over the years, so check the CRI-O packaging README for the current baseurl before pasting. That ten-second check saves a lot of “repomd.xml not found” frustration.

Fedora also carries its own cri-o and kubernetes-* packages, which some admins prefer for tighter distro integration. They lag the upstream project and use different package names (kubernetes-kubeadm, kubernetes-node, kubernetes-client). Mixing the two sources on one node is a bad idea. Pick one lineage and stay with it. This guide uses upstream packages because they track the Kubernetes release schedule and match the official documentation.

Step 7: Install CRI-O and the Kubernetes Components

Why CRI-O instead of containerd or Docker? Docker Engine needs an extra shim to talk to the kubelet, so it is not a natural fit. CRI-O is purpose-built for Kubernetes, has a small footprint and follows Kubernetes’ version numbers. Containerd is equally valid. If your team already runs it elsewhere, use it. The kubeadm steps are the same either way.

Install the prerequisites and packages. Fedora 44 uses DNF5, which changed the flag for temporarily lifting repository excludes:

sudo dnf install -y container-selinux conntrack-tools ethtool iptables socat iproute-tc

sudo dnf install -y cri-o kubelet kubeadm kubectl \
  --setopt=disable_excludes=kubernetes

If you copy a command from an older guide that says --disableexcludes=kubernetes, it applies to DNF4 systems, meaning Fedora before 41. On Fedora 44 use the --setopt form shown above. The kubeadm docs also mention adding --setopt=install_weak_deps=False to avoid pulling optional extras, which is handy on minimal nodes.

Enable the runtime and the kubelet:

sudo systemctl enable --now crio
sudo systemctl enable --now kubelet

Do not panic when the kubelet restarts every few seconds right now. It is waiting for instructions from kubeadm, and it will settle down once the node is initialized or joined. Check the runtime though:

sudo systemctl status crio --no-pager
sudo crictl --runtime-endpoint unix:///var/run/crio/crio.sock info | head

Confirm the cgroup driver

Modern Fedora uses systemd and cgroup v2. Both CRI-O and the kubelet must use the systemd cgroup driver, or you will see instability under load. CRI-O defaults to systemd on these systems, and recent kubeadm versions default the kubelet to systemd too. Verify the host:

stat -fc %T /sys/fs/cgroup/

The output should read cgroup2fs. If you ever need to be explicit, drop a file in /etc/crio/crio.conf.d/:

sudo tee /etc/crio/crio.conf.d/02-cgroup.conf <<'EOF'
[crio.runtime]
cgroup_manager = "systemd"
EOF
sudo systemctl restart crio

Step 8: Initialize the Control Plane

Everything so far was preparation. Now run kubeadm on k8s-cp1 only.

A config file beats command-line flags because it is reviewable and reproducible. Create kubeadm-config.yaml:

apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 192.168.50.10
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///var/run/crio/crio.sock
  name: k8s-cp1
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: stable
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12
controlPlaneEndpoint: "k8s-cp1:6443"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd

Pin kubernetesVersion to the exact version you installed (for example the output of kubeadm version -o short) rather than leaving stable. If the installed binaries and the requested control plane image versions drift, kubeadm will warn or fail. Check the schema version too: v1beta4 is current for recent releases, but run kubeadm config print init-defaults to see what your binary expects.

Pre-pull the images, which also validates registry connectivity:

sudo kubeadm config images pull --config kubeadm-config.yaml

Then bootstrap:

sudo kubeadm init --config kubeadm-config.yaml --upload-certs

This takes a couple of minutes. On success, kubeadm prints a kubeadm join command containing a token and a discovery hash. Copy it somewhere safe. Tokens expire after 24 hours by default, but you can regenerate one later with kubeadm token create --print-join-command.

Configure kubectl for your regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Check the state:

kubectl get nodes
kubectl get pods -A

The node will show NotReady and CoreDNS pods will sit in Pending. That is expected. Without a pod network, there is nothing for them to attach to.

Step 9: Install a Pod Network (CNI)

A cluster without a CNI plugin is just a collection of confused processes. Choose based on need:

CNI Best for Network policy Notes
Flannel Labs, small clusters No (needs add-on) Simplest, VXLAN overlay
Calico General production Yes BGP or VXLAN, mature
Cilium Advanced observability, eBPF Yes Needs a recent kernel, which Fedora provides

For a first build, Flannel is the least fiddly. Apply the manifest from the project’s releases page:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

Flannel’s default pod network is 10.244.0.0/16, which is why the podSubnet in the kubeadm config above uses that value. If you picked a different CIDR, download the manifest and edit the net-conf.json block before applying it. A mismatch between the two is a very common reason pods get addresses that cannot route.

Watch the rollout:

kubectl get pods -n kube-flannel -w
kubectl get nodes

Within a minute or two the control plane node flips to Ready and CoreDNS starts.

If network policies matter to you, which they should in any multi-tenant setup, skip Flannel and install Calico or Cilium instead. Retrofitting a CNI later means draining nodes and restarting every pod, so decide now.

Step 10: Join the Worker Nodes

Complete Steps 1 through 7 on each worker. That means swap off, modules loaded, sysctl applied, firewall ports opened, repos configured, and CRI-O plus kubelet and kubeadm installed. You do not need kubectl on workers, though it does no harm.

Then run the join command that kubeadm printed:

sudo kubeadm join k8s-cp1:6443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

If the token expired, regenerate it on the control plane:

kubeadm token create --print-join-command

Verify from the control plane:

kubectl get nodes -o wide

You should see three nodes in Ready state after a short wait. Label the workers so output is easier to read:

kubectl label node k8s-w1 node-role.kubernetes.io/worker=
kubectl label node k8s-w2 node-role.kubernetes.io/worker=

Test with a real workload

A green node list proves little. Deploy something and hit it:

kubectl create deployment web --image=nginx:stable --replicas=3
kubectl expose deployment web --type=NodePort --port=80
kubectl get svc web

Take the NodePort from the output and request it from your workstation:

curl -I http://192.168.50.11:<nodeport>

A 200 response from every node IP confirms that the overlay network, kube-proxy and the firewall rules are all behaving. Also test DNS from inside the cluster:

kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- nslookup kubernetes.default

Troubleshooting Common Fedora Kubernetes Errors

Experience says that roughly 80 percent of failed bootstraps come from the same handful of problems. Work through these in order.

Kubelet keeps crash-looping after kubeadm init

Read the logs first, not forums:

sudo journalctl -u kubelet -n 100 --no-pager

The usual causes are swap being active (look for “running with swap on is not supported”), a cgroup driver mismatch between kubelet and runtime, or a wrong criSocket path. Re-check Step 2 and Step 7.

“[ERROR FileContent–proc-sys-net-bridge-bridge-nf-call-iptables]”

The br_netfilter module is not loaded or the sysctl is unset. Re-run the commands from Step 3. If it returns after reboot, check that /etc/modules-load.d/k8s.conf exists.

Node stays NotReady after installing the CNI

Describe the node and inspect the CNI pods:

kubectl describe node k8s-w1 | sed -n '/Conditions/,/Addresses/p'
kubectl logs -n kube-flannel -l app=flannel --tail=50

Typical culprits are a blocked UDP 8472 on firewalld, a pod CIDR mismatch, or /etc/cni/net.d containing a stale config from a previous attempt. Remove leftover files there when rebuilding.

Pods cannot resolve DNS or reach services

Confirm kube-proxy is running and check CoreDNS logs. On Fedora, firewalld masquerading can get in the way. Enabling it on the zone often resolves it:

sudo firewall-cmd --permanent --add-masquerade
sudo firewall-cmd --reload

If the cluster worked until a reboot, suspect firewalld reloading and dropping interface assignments, or NetworkManager grabbing CNI interfaces. A drop-in file in /etc/NetworkManager/conf.d/ marking flannel*, cali* and cilium* interfaces as unmanaged prevents that:

sudo tee /etc/NetworkManager/conf.d/k8s-cni.conf <<'EOF'
[keyfile]
unmanaged-devices=interface-name:flannel*;interface-name:cni*;interface-name:cali*;interface-name:vxlan.calico;interface-name:cilium*
EOF
sudo systemctl reload NetworkManager

“repomd.xml not found” or GPG errors when installing

Refresh metadata with sudo dnf clean all && sudo dnf makecache, a fix that has been needed since the earliest pkgs.k8s.io rollout. Then double check that the minor version in your baseurl exists. Version strings must be written like v1.37, not 1.37.

Image pull failures

CRI-O pulls from registries using containers-common policy files. If you sit behind a proxy, set it through a systemd drop-in for crio.service, not just your shell environment:

sudo systemctl edit crio
[Service]
Environment="HTTP_PROXY=http://proxy.example.local:3128"
Environment="HTTPS_PROXY=http://proxy.example.local:3128"
Environment="NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,10.244.0.0/16,192.168.50.0/24,.svc,.cluster.local"

SELinux denials

When enforcing is on and a pod fails with permission errors on a mounted path, look at ausearch -m avc, and relabel the host directory with chcon -Rt container_file_t /path or a proper semanage fcontext rule. Disabling SELinux to make an error vanish is the lazy answer. Fix the label.

Starting over cleanly

Sometimes the fastest route is a reset:

sudo kubeadm reset -f
sudo rm -rf /etc/cni/net.d $HOME/.kube
sudo systemctl restart crio

Reboot after that to clear iptables or nftables state left behind by kube-proxy.

r00t is an experienced Linux enthusiast and technical writer with a passion for open-source software. With years of hands-on experience in various Linux distributions, r00t has developed a deep understanding of the Linux ecosystem and its powerful tools. He holds certifications in SCE and has contributed to several open-source projects. r00t is dedicated to sharing her knowledge and expertise through well-researched and informative articles, helping others navigate the world of Linux with confidence.

Related Posts