How to Install Kubernetes with kubeadm on Ubuntu 26.04

When you outgrow a single Docker host and need to run containers across several machines with rolling updates, self-healing, and service discovery built in, Kubernetes is the standard answer. The kubeadm tool provides a minimal upstream method for bootstrapping a self-managed cluster on plain Linux servers.
This guide explains how to set up Kubernetes 1.36 on Ubuntu 26.04 using kubeadm, kubelet, and kubectl from the Kubernetes apt repository, 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 running across the cluster.
Quick Reference
| Task | Command |
|---|---|
| Disable swap | sudo swapoff -a |
| Install containerd | sudo apt install -y containerd |
| Add Kubernetes apt key | curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg |
| Install kube tools | sudo apt install -y kubelet kubeadm kubectl |
| Hold kube versions | sudo apt-mark hold kubelet kubeadm kubectl |
| Init control plane | sudo kubeadm init --pod-network-cidr=10.244.0.0/16 |
| Configure kubectl | mkdir -p ~/.kube && sudo cp /etc/kubernetes/admin.conf ~/.kube/config && sudo chown $(id -u):$(id -g) ~/.kube/config |
| Get nodes | kubectl get nodes |
| Get pods (all namespaces) | kubectl get pods -A |
| Generate join command | sudo kubeadm token create --print-join-command |
Prerequisites
Before starting, make sure you have:
- Two or more servers running Ubuntu 26.04 with at least 2 GB of RAM each. The control-plane node needs at least 2 CPUs.
- A user with sudo privileges on each server.
- Full network connectivity between the nodes and Internet access for downloading packages and container images.
- A stable IP address plus a unique hostname, MAC address, and product UUID on every node.
The same instructions apply to control-plane and worker nodes up to the cluster bootstrap step. Run every command on every node unless the section says otherwise.
If a host or cloud firewall filters traffic between the nodes, allow the required traffic between these sources and destination ports:
| Destination | Source | Ports |
|---|---|---|
| Control plane API | Cluster nodes and administrators | TCP 6443 |
| etcd on the control plane | Control-plane nodes only | TCP 2379-2380 |
| Control-plane kubelet | Control-plane components | TCP 10250 |
| Scheduler and controller manager | Control-plane node itself | TCP 10257 and 10259 |
| Worker kubelet | Control-plane nodes | TCP 10250 |
| kube-proxy health endpoint | Node itself and load balancers that use it | TCP 10256 |
| NodePort services | Clients that need access | TCP and UDP 30000-32767 |
| Flannel VXLAN | Other cluster nodes | UDP 8472 |
Restrict these rules to trusted cluster and administration networks. Do not expose the etcd or kubelet ports directly to the Internet.
Step 1: Prepare the System
The kubelet refuses to start when swap is active unless you configure it to tolerate swap. This guide uses the default behavior, so turn swap off for the current session and comment out swap entries in /etc/fstab:
sudo swapoff -a
sudo sed -i '/[[:space:]]swap[[:space:]]/ s/^[^#]/#&/' /etc/fstabCheck that no active swap devices remain:
swapon --showThe command should return no output.
Load the kernel modules required by the container runtime and Kubernetes networking:
sudo tee /etc/modules-load.d/k8s.conf <<EOF
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilterConfirm that both modules are loaded:
lsmod | grep -E 'br_netfilter|overlay'Enable the sysctl settings that allow iptables to see bridged traffic and let IP forwarding work:
sudo tee /etc/sysctl.d/k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --systemVerify the two main networking settings:
sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptablesBoth values should be 1.
Step 2: Install containerd
Kubernetes uses the Container Runtime Interface (CRI) to communicate with a container runtime. Ubuntu 26.04 provides containerd 2.x in its repositories:
sudo apt update
sudo apt install -y containerdIf Docker is already installed from the official Docker repository, the containerd.io package provides the same runtime and conflicts with the containerd package. Skip the install above and continue with the configuration below.
Generate a complete containerd configuration, then enable the systemd cgroup driver so it matches the kubelet configuration created by kubeadm:
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerdConfirm that the service is running and the setting is present:
systemctl is-active containerd
sudo grep 'SystemdCgroup = true' /etc/containerd/config.tomlThe first command should print active, and the second should show the enabled cgroup setting.
Step 3: Add the Kubernetes apt Repository
Kubernetes publishes a separate package repository for every minor release. This guide follows the supported v1.36 series, and apt installs the newest patch release available in that repository.
Install the prerequisites and download the signing key:
sudo apt install -y apt-transport-https ca-certificates curl gpg
sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpgAdd the repository:
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.listRefresh the package index:
sudo apt updateStep 4: Install kubelet, kubeadm, and kubectl
Install the three Kubernetes tools and pin their versions so an apt upgrade does not change them by accident:
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectlConfirm the install:
kubeadm version
kubelet --version
kubectl version --clientThe apt-mark hold flag is important because Kubernetes upgrades follow a specific procedure (kubeadm upgrade), and a casual apt upgrade could break the cluster.
The kubelet may restart repeatedly at this point because it does not have a cluster configuration yet. This is expected and stops after kubeadm init or kubeadm join configures the node.
Step 5: Initialize the Control Plane
Run the next two steps only on the control-plane node.
--control-plane-endpoint before initialization.Pick a CIDR for the pod network that does not overlap with your host network. The example uses 10.244.0.0/16, which is the default for Flannel. Bootstrap the cluster:
sudo kubeadm init --pod-network-cidr=10.244.0.0/16The command pulls the control-plane container images, generates certificates, writes the kubeconfig files under /etc/kubernetes/, and starts the static pods for the API server, controller manager, scheduler, and etcd. When it finishes you will see a block similar to this:
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a 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
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 192.168.1.10:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:1234...Copy the join command somewhere safe. The bootstrap token is a credential that allows a node to authenticate while joining the cluster, so do not publish or share it with untrusted users.
Set up the kubeconfig for your sudo user so kubectl works without root:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/configThe copied admin.conf grants cluster administrator access. Keep ~/.kube/config private and do not distribute it as a general user credential.
Verify that the API server is reachable:
kubectl get nodesThe output lists the control-plane node with a status of NotReady. The status changes to Ready after you install a pod network in the next step.
Step 6: Install a Pod Network
Pods cannot communicate until the cluster has a network plugin. Flannel is one of the simplest options that works with the default 10.244.0.0/16 CIDR:
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.ymlWatch the system pods come up:
kubectl get pods -n kube-flannel
kubectl get pods -n kube-system
kubectl get nodesAfter a minute or so, the Flannel and core system pods should report Running, and the control-plane node should show Ready.
If you prefer Calico, Cilium, or another CNI plugin, install it now instead of Flannel and pass the matching --pod-network-cidr value to kubeadm init.
Step 7: Join Worker Nodes
Run this step on each worker node.
Use the kubeadm join command that was printed at the end of kubeadm init. If you lost it or the token has expired (tokens are valid for 24 hours by default), generate a new one on the control-plane node:
sudo kubeadm token create --print-join-commandThen run the printed command on the worker:
sudo kubeadm join 192.168.1.10:6443 --token <token> \
--discovery-token-ca-cert-hash sha256:<hash>Back on the control-plane node, verify that the new node has joined:
kubectl get nodesThe output now lists the worker. It moves from NotReady to Ready once the kubelet finishes its registration and the network plugin reports healthy.
Step 8: Test the Cluster
Deploy a small workload to confirm the cluster schedules pods and that networking is working:
kubectl create deployment nginx-test --image=nginx
kubectl expose deployment nginx-test --port=80 --type=NodePort
kubectl rollout status deployment/nginx-test
kubectl get pods,svcThe output shows the nginx-test pod in the Running state and a NodePort service with a port in the 30000-32767 range:
NAME READY STATUS RESTARTS AGE
pod/nginx-test-7b7bf6d9b8-8pxnf 1/1 Running 0 25s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginx-test NodePort 10.105.10.123 <none> 80:31234/TCP 12sOpen http://<node_ip>:<nodeport> in a browser and you should see the Nginx welcome page. Tear the demo down once you confirm it works:
kubectl delete service nginx-test
kubectl delete deployment nginx-testTroubleshooting
kubeadm init fails with “container runtime is not running”containerd is not running, or its CRI plugin is disabled. Inspect the service with sudo systemctl status containerd --no-pager and check /etc/containerd/config.toml for a disabled_plugins entry containing cri. Remove cri from that list, confirm that SystemdCgroup = true, and restart the service with sudo systemctl restart containerd.
Nodes stay NotReady after init
The pod network is not installed or the CNI pods are crashing. Run kubectl get pods -n kube-flannel and kubectl logs -n kube-flannel daemonset/kube-flannel-ds --tail=50 to inspect Flannel. Confirm that the --pod-network-cidr passed to kubeadm init matches the CIDR expected by the CNI plugin.
Cgroup driver mismatch warning in kubelet logs
The kubelet uses systemd and the runtime uses cgroupfs, or vice versa. Edit /etc/containerd/config.toml, set SystemdCgroup = true, and restart containerd. Restart the kubelet with sudo systemctl restart kubelet.
The connection to the server localhost:8080 was refusedkubectl is reading the wrong kubeconfig. Run kubectl config view to confirm the current context, and make sure ~/.kube/config exists for the user running the command. On the control-plane node, copy /etc/kubernetes/admin.conf to ~/.kube/config and fix ownership.
Worker join times out
The control-plane host is not reachable from the worker on TCP port 6443, or the token has expired. Confirm the route and firewall rules, then run sudo kubeadm token create --print-join-command on the control plane to generate a fresh join command.
The Nginx NodePort does not respond
The workload can be healthy while a host or cloud firewall blocks the assigned NodePort. Read the port from kubectl get service nginx-test, then allow that TCP port only from the client network that needs access.
FAQ
Do I still need Docker?
No. Kubernetes communicates with container runtimes through the CRI, and containerd is enough for this cluster. Kubernetes 1.24 removed the built-in Docker integration known as dockershim. If you also want to build images on the same host, install Docker as a separate tool. See How to Install Docker on Ubuntu 26.04
for the build environment.
Why is swap disabled?
The kubelet refuses to start by default when it detects active swap. Kubernetes can use swap with an explicit kubelet configuration, but disabling it keeps this installation aligned with the default behavior.
Can I run a single-node cluster on the control plane?
Yes. Remove the control-plane taint with kubectl taint nodes --all node-role.kubernetes.io/control-plane-. After that, the scheduler places workload pods on the control-plane node.
How do I upgrade Kubernetes later?
Follow the kubeadm node upgrade procedure and move through one minor version at a time. The process includes upgrading kubeadm, running kubeadm upgrade plan and kubeadm upgrade apply, then upgrading the kubelet and kubectl. The package holds prevent these components from changing during a routine apt upgrade.
Next Steps
You now have a working Kubernetes cluster on Ubuntu 26.04. From here you can deploy applications with manifests or Helm charts, set up an ingress controller, and add storage classes that match your environment. For a single-node lab on the same host, an alternative is to start with k3s or minikube and migrate to a kubeadm cluster as your needs grow.
Tags
Linuxize Weekly Newsletter
A quick weekly roundup of new tutorials, news, and tips.
About the authors

Dejan Panovski
Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 800+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.
View author page