Kubernetes is the standard platform for running containerized workloads across multiple servers with self-healing, rolling updates, and built-in service discovery. kubeadm is the upstream tool for bootstrapping a minimal cluster on plain Linux servers without managed cloud infrastructure.
This guide explains how to install Kubernetes on Ubuntu 26.04 using kubeadm, kubelet, and kubectl, with containerd as the container runtime. By the end you will have a control-plane node, one or more worker nodes, and a test workload confirmed running across the cluster.
Prerequisites: Two or more Ubuntu 26.04 servers with at least 2 GB RAM each (the control-plane node also needs 2 CPUs), sudo access, full network connectivity between nodes, and a stable IP on every machine. Run every command on all nodes unless the section says otherwise.
Install Kubernetes on Ubuntu: Prepare the System and Container Runtime
Disable swap. The kubelet refuses to start when swap is active under its default configuration. Disable it for the current session and remove the swap entries from /etc/fstab so it stays off after a reboot:
bash
sudo swapoff -a<br>sudo sed -i '/[[:space:]]swap[[:space:]]/ s/^[^#]/#&/' /etc/fstab<br>Load the required kernel modules. The overlay module supports OverlayFS, the default container filesystem driver. The br_netfilter module lets iptables see bridged network traffic, which Kubernetes needs for service routing and NetworkPolicies:
bash
sudo tee /etc/modules-load.d/k8s.conf <<EOF<br>overlay<br>br_netfilter<br>EOF<br>sudo modprobe overlay && sudo modprobe br_netfilter<br>Enable IP forwarding and bridge filtering:
bash
sudo tee /etc/sysctl.d/k8s.conf <<EOF<br>net.bridge.bridge-nf-call-iptables = 1<br>net.bridge.bridge-nf-call-ip6tables = 1<br>net.ipv4.ip_forward = 1<br>EOF<br>sudo sysctl --system<br>Install and configure containerd. Kubernetes communicates with container runtimes through the Container Runtime Interface (CRI). Ubuntu 26.04 provides containerd 2.x in its default repositories:
bash
sudo apt update && sudo apt install -y containerd<br>sudo mkdir -p /etc/containerd<br>containerd config default | sudo tee /etc/containerd/config.toml > /dev/null<br>sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml<br>sudo systemctl restart containerd && sudo systemctl enable containerd<br>Setting SystemdCgroup = true is not optional. The kubelet and the container runtime must use the same cgroup driver. Ubuntu uses systemd as its init system, so both must be set to the systemd cgroup driver. A mismatch causes pod failures that appear after the cluster is already running and are difficult to trace.
Add the Kubernetes Repository, Install Tools, and Initialize the Control Plane
Add the Kubernetes v1.36 apt repository. Kubernetes publishes a separate repository per minor version so an apt upgrade cannot accidentally move you across a minor version boundary:
bash
sudo apt install -y apt-transport-https ca-certificates curl gpg<br>sudo mkdir -p -m 755 /etc/apt/keyrings<br>curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg<br>echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list<br>sudo apt update<br>Install and hold the Kubernetes tools:
bash
sudo apt install -y kubelet kubeadm kubectl<br>sudo apt-mark hold kubelet kubeadm kubectl<br>The apt-mark hold step is critical. Kubernetes upgrades run through kubeadm's own upgrade procedure — a plain apt upgrade skips it and breaks the cluster. The kubelet restart loop you see immediately after install is expected; it stops once kubeadm init or kubeadm join provides the cluster configuration.
Initialize the control plane (control-plane node only). Choose a pod CIDR that does not overlap your host network. The example uses 10.244.0.0/16, which is Flannel’s default:
bash
sudo kubeadm init --pod-network-cidr=10.244.0.0/16<br>When the command finishes, it prints a kubeadm join command with a bootstrap token. Save this output. The token authenticates worker nodes during the join and expires after 24 hours.
Set up kubectl access for your regular user:
bash
mkdir -p $HOME/.kube<br>sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config<br>sudo chown $(id -u):$(id -g) $HOME/.kube/config<br>The copied admin.conf grants full cluster administrator access. Keep ~/.kube/config private.
Install a Pod Network, Join Worker Nodes, and Test the Cluster
Install Flannel (control-plane node only). Pods cannot communicate until a CNI network plugin is running. Running kubectl get nodes right after kubeadm init shows the control-plane node as NotReady for this reason:
bash
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml<br>kubectl get nodes<br>The node moves to Ready once the Flannel pods are running. If you prefer Calico or Cilium, install it instead and pass the matching CIDR to kubeadm init.
Join each worker node using the command saved from the init output. If the token has expired, generate a new one on the control-plane node:
bash
sudo kubeadm token create --print-join-command<br>Run the printed kubeadm join command on the worker. Verify it joined: kubectl get nodes.
bash
kubectl create deployment nginx-test --image=nginx<br>kubectl expose deployment nginx-test --port=80 --type=NodePort<br>kubectl rollout status deployment/nginx-test<br>kubectl get pods,svc<br>The NodePort service assigns a port in the 30000–32767 range. Open http://: to confirm the Nginx welcome page loads. Remove the test workload: kubectl delete service nginx-test && kubectl delete deployment nginx-test.
The cluster is ready. From here, deploy applications with Helm charts, add an ingress controller, and configure storage classes for persistent volumes. For a single-node lab, k3s or minikube are simpler starting points before committing to a kubeadm setup. Leave a comment below if you run into any issues.