How to Install Kubernetes on Ubuntu 26.04 LTS
For slightly over a decade, Kubernetes has been the undisputed king of software deployment. Before the Docker and Kubernetes era, applications were built as large, single-unit codebase systems. This monolithic architecture had multiple pain points, including inefficient scaling, low fault tolerance, and a higher risk of downtime in case of a single bug.
With Docker, applications are decoupled into multiple, atomized units running independently inside containers. Kubernetes provides a control layer over Docker by managing these containers.
Kubernetes aggregates multiple servers into a single cluster, allocating resources to the nodes to run the containers. It automates various aspects of container management, including auto-scaling, self-healing, and load-balancing, ensuring high availability and minimal downtime.
In this guide, you will learn how to set up a Kubernetes cluster and deploy a simple application to test the cluster’s functionality. All the nodes will be running Ubuntu 26.04 LTS.
#Prerequisites
For this lab, we have a 2-node setup comprising a master and a worker node. All nodes are running Ubuntu 26.04 LTS.
Master node k8s-master-node 10.190.104.1
Worker node 1 k8s-worker-node 10.190.104.39
Here are the minimum requirements for both nodes:
-
Minimum 2 CPU cores and 2 GB RAM (4 GB recommended)
-
SSH access to both nodes
-
A sudo user configured on all nodes
To follow along, you can create your lab by deploying cloud servers at Cherry Servers
Scalable VPS Hosting
Deploy fast, secure VPS for websites, apps, and dev environments. Scale on demand, manage with full root access, and get 24/7 expert support.
Now that the groundwork is laid, we'll walk through setting up and configuring Kubernetes on a two-node cluster: one master and one worker node. At the tail end, we will deploy a simple web app to test the cluster's functionality.
#Step 1: Disable swap (For all nodes)
Swap is discouraged because it degrades performance in clusters. Accessing memory from swap is significantly slower (up to 1000x slower) than from physical RAM, causing severe performance degradation. Also, Swap interferes with Kubernetes' QoS tiers. Applications expecting immediate, guaranteed resources might suddenly slow down if another process forces swapping.
The general rule of thumb is to disable swap to avoid performance bottlenecks.
sudo swapoff -a
This works temporarily. To persist the swap removal across reboots, remove any swap entries from the/etc/fstab file.
sudo sed -i '/ swap / s/^/#/' /etc/fstab
Then reboot the system for changes to take effect.
sudo reboot
Confirm that swap has been fully disabled:
free -h
The Swap row in the output should show all zeros.
#Step 3: Configure /etc/hosts file (all nodes)
Kubernetes relies on hostnames to distinguish nodes. When a worker node comes online and joins the cluster, it announces itself using its hostname — so if hostname resolution breaks down, nodes lose the ability to locate and talk to one another.
With this in mind, update the /etc/hosts file. The IP and hostname entries give every node a reliable way to resolve and communicate with other nodes.
sudo cat >> /etc/hosts << 'EOF'
10.190.104.1 k8s-master-node
10.190.104.39 k8s-worker-node
EOF
Try pinging the nodes from each other to ensure they can communicate seamlessly.
On the master node:
ping -c 4 k8s-worker-node
On the worker node:
ping -c 4 k8s-master-node
In both cases, you should get a positive ping reply indicating communication between the nodes.
#Step 4: Load Kernel Modules and Configure Sysctl (all nodes)
Kubernetes networking depends on two kernel modules: overlay (used by containerd for layered filesystems) and br_netfilter (required for bridge networking and iptables rules on bridged traffic). You need to load them and configure them to load automatically on boot.
To do this, first, create a configuration file and specify the overlay and br_netfilter modules.
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
Next, enable the modules using the modprobe command.
sudo modprobe overlay
sudo modprobe br_netfilter
Be sure to verify both modules are loaded:
lsmod | grep -E "overlay|br_netfilter"
You should see both listed in the output:
Outputbr_netfilter 32768 0
bridge 425984 1 br_netfilter
overlay 233472 0
Next, configure the required sysctl parameters for bridge networking and IP forwarding:
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
Then run the command:
sudo sysctl --system
To verify all the parameters are active, simply run:
sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
All three values should return 1.
#Step 5: Install the Container Runtime (containerd)(all nodes)
Kubernetes requires a CRI-compatible container runtime. Containerd is the standard and recommended choice. It ships in Docker's apt repository with the latest stable builds.
But first things first, update the local package index and install the prerequisites.
sudo apt update
sudo apt install ca-certificates curl gnupg -y
The next step is to add Docker's GPG key. Create the keyrings directory into which the GPG key will go.
sudo install -m 0755 -d /etc/apt/keyrings
Then download the GPG key and store it in the keyrings directory.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Next, make the GPG key readable to all by altering its permissions.
sudo chmod a+r /etc/apt/keyrings/docker.gpg
Next, add Docker’s apt repository to your sources directory:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Once added, install containerd as follows:
sudo apt update && sudo apt install -y containerd.io
Confirm the installed version:
containerd --version
You should get output similar to what we have:
Outputcontainerd containerd.io v2.2.5 e53c7c1516c3b2bff98eb76f1f4117477e6f4e66
#Step 6: Configure containerd for cgroup v2 (all nodes)
cgroups (Control Groups) is a Linux kernel feature that limits, accounts for, and isolates resources, including memory, CPU, network bandwidth, and disk I/O.
Kubernetes relies on cgroups heavily to:
- Enforce pod resource requests and limits
- Prevent a single pod from consuming all node resources
- Report resource usage via kubectl top
- Enable the kubelet to manage the container lifecycle
cgroupv2 is the second generation of cgroups and comes with improved features such as enhanced memory management and full support for rootless containers.
Even though cgroupv2 is pre-configured, you need to ensure containerd and kubelet use the same cgroup driver (systemd is recommended). To achieve this, begin by generating the default containerd configuration file:
sudo containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
Enable the systemd cgroup driver by changing SystemdCgroup from false to true:
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
Confirm the change took effect:
grep SystemdCgroup /etc/containerd/config.toml
The output should show:
OutputSystemdCgroup = true
Next, restart and enable containerd to apply the new configuration:
sudo systemctl restart containerd
sudo systemctl enable containerd
sudo systemctl is-active containerd
#Step 7: Install kubeadm, kubelet, and kubectl (all nodes)
In this step, we will proceed to install the core Kubernetes packages: kubeadm, kubelet, and kubectl.
Let’s have a quick overview of what each component does:
-
kubeadm: bootstraps the Kubernetes cluster
-
kubelet: runs on every node and manages container workloads
-
kubectl: The command-line tool for interacting with the cluster
Now, download and store the Kubernetes GPG key:
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
Next, add the Kubernetes apt repository to the sources directory.
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
Once done, update the package index and install kubelet, kubedm and kubectl.
sudo apt update
sudo apt install kubelet kubeadm kubectl -y
#Step 8: Initialize the Kubernetes cluster (on the master node)
With all prerequisites in place, bootstrap the control plane using kubeadm init command. The --pod-network-cidr flag sets the IP range for pod networking, which the CNI plugin will use:
sudo kubeadm init --pod-network-cidr=10.200.50.0/16
This process pulls the required container images, generates TLS certificates, and starts the control plane components as static pods. It typically takes about one minute.
The output contains instructions on how you can start using your cluster. At the very end, you will see the command to run on worker nodes to join them to the cluster. This command will be different in your case. Copy it and paste it somewhere, as you will need it to join the worker node to the cluster.
kubeadm join 84.32.83.177:6443 --token ape2tj.tobkivq4muq5h8e5 \
--discovery-token-ca-cert-hash sha256:d052f0fe26c62520957f7d083bc8b82f20ff1928ad3980fe4130cba9edd6c659
To finish up, run the following commands to start using the cluster.
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Be sure to verify the node status:
kubectl get nodes
The node will show NotReady at this point. This is expected because no CNI network plugin has been installed yet:
NAME STATUS ROLES AGE VERSION
k8s-master-node NotReady control-plane 4m11s v1.33.13
#Step 9: Install the Calico CNI plugin (on master node)
CNI (Container Network Interface) is a standard that defines how networking is set up for containers in Kubernetes. Without a CNI plugin, pods cannot communicate with each other. Kubernetes deliberately ships without a built-in network implementation. It defines the rules but delegates the actual networking to a CNI plugin.
Calico is the most widely adopted CNI owing to its numerous benefits. It combines
true network policy enforcement, high-performance routing without overlays, and production-scale reliability making it the default choice for anything beyond basic local development.
To install Calico, begin by installing the Tigera operator from a YAML file
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/tigera-operator.yaml
Next, download Calico’s custom-resources file. This is a Calico-specific manifest that defines the initial configuration of your Calico installation through Kubernetes Custom Resource Definitions (CRDs). It tells Calico how to set itself up after its core components are running.
curl https://raw.githubusercontent.com/projectcalico/calico/v3.29.3/manifests/custom-resources.yaml -O
Next, modify the CIDR of the manifest from the default 192.168.0.0/16 to match your cluster's network subnet.
sed -i 's/cidr: 192\.168\.0\.0\/16/cidr: 10.200.50.0\/16/g' custom-resources.yaml
Finally, apply the YAML manifest file.
kubectl create -f custom-resources.yaml
Check the node status again:
kubectl get nodes
You can retrieve more detailed information using the -o wide option.
kubectl get nodes -o wide
The node should now show Ready status:
NAME STATUS ROLES AGE VERSION
k8s-master-node Ready control-plane 8m51s v1.33.13
#Step 10: Add worker nodes to the cluster (On worker nodes)
To add a worker node to the cluster, paste the kubeadm command that you copied in step 7 when initializing the cluster.
kubeadm join 84.32.83.177:6443 --token ape2tj.tobkivq4muq5h8e5 \
--discovery-token-ca-cert-hash sha256:d052f0fe26c62520957f7d083bc8b82f20ff1928ad3980fe4130cba9edd6c659
You should see a flurry of output on your screen and finally, you’ll get a notification that the node is now part of the cluster.
Back to the master node, verify the worker node has joined the cluster:
kubectl get nodes
This time around, you should see two nodes with a Ready status.
NAME STATUS ROLES AGE VERSION
k8s-master-node Ready control-plane 20m v1.33.13
k8s-worker-node Ready <none> 23s v1.33.13
The Cluster is now fully set up and ready for application deployment.
#Step 11: Testing the Cluster (on the master node)
In this step, we will deploy a simple Nginx web app to test the cluster’s functionality.
By default, the master/control-plane node is automatically tainted to prevent regular workload pods from being scheduled on it. This is done intentionally to prevent resource contention from additional workloads, which can starve the API server.
To untaint it, run:
kubectl taint nodes k8s-master-node node-role.kubernetes.io/control-plane:NoSchedule-
Next, create a namespace in which you’ll deploy the web app. In this case, the namespace is called webtest.
kubectl create namespace webtest
kubectl config set-context --current --namespace=webtest
Create a YAML file for the deployment resource. In this case, our file is named web-deployment.yaml.
sudo nano web-deployment.yaml
Add the following lines of code to the file.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-app
namespace: webtest
spec:
replicas: 2 # One pod per node
selector:
matchLabels:
app: nginx-app
template:
metadata:
labels:
app: nginx-app
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
Launch the deployment as shown.
kubectl apply -f web-deployment.yaml
Ensure the pods are running in both the master and worker nodes by running:
kubectl get pods -n webtest -o wide
Next, expose the application using a NodePort service. To do this, create a service YAML file.
sudo nano web-service.yaml
Add the following lines of code.
apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: webtest
spec:
type: NodePort
selector:
app: nginx-app
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080 # Access via this port on any node IP
Launch the service.
kubectl apply -f web-service.yaml
Then confirm the service is running in the webtest namespace.
kubectl get svc -n webtest
OutputNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-service NodePort 10.111.24.99 <none> 80:30080/TCP 10s
Finally, confirm you can access the Nginx application via NodePort using the cURL command from either node.
# Via master node
curl http://master-node-ip:30080
# Via worker node
curl http://worker-node-ip:30080
Similarly, you can access the application via a web browser:
# Via master node
http://master-node-ip:30080
# Via worker node
http://worker-node-ip:30080
Both should return the Nginx welcome page. This confirms the cluster is running as expected.
#Conclusion
You have successfully set up a fully functional Kubernetes cluster on Ubuntu 26.04. This setup is ideal for learning Kubernetes concepts, developing applications, and testing deployments before moving to production environments.
For your next steps, consider using Helm charts to deploy containerized applications and adding worker nodes for a true multi-node cluster. Additionally, you can integrate open-source stacks like Prometheus and Grafana for cluster observability.
Starting at just $3.51 / month, get virtual servers with top-tier performance.