<\/span><\/h3>\n\n\n\nKubelet can’t reliably enforce memory limits or evict pods under pressure if swap is active, so Kubernetes requires it to be off before the cluster will even initialize.<\/p>\n\n\n\n
sudo swapoff -a<\/pre>\nsudo sed -i '\/ swap \/ s\/^\/#\/' \/etc\/fstab<\/pre>\n\n\n\n <\/figure>\n\n\n\n<\/span>Load Required Kernel Modules<\/strong><\/span><\/h3>\n\n\n\nThe overlay module enables containerd’s layered filesystem, and br_netfilter lets bridged network traffic be seen and filtered by iptables; both are required for pod networking to function.<\/p>\n\n\n\n
cat <<EOF | sudo tee \/etc\/modules-load.d\/k8s.conf overlay br_netfilter EOF<\/pre>\nsudo modprobe overlay<\/pre>\nsudo modprobe br_netfilter<\/pre>\n\n\n\n <\/figure>\n\n\n\n<\/span>Configure Required sysctl Parameters.<\/strong><\/span><\/h3>\n\n\n\nThese settings ensure that iptables processes traffic crossing network bridges and that the kernel forwards packets between pods, which Kubernetes networking depends on.<\/p>\n\n\n\n
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<\/pre>\n\n\n\n <\/figure>\n\n\n\nThese settings ensure traffic crossing network bridges is correctly processed by iptables, which Kubernetes networking depends on.<\/p>\n\n\n\n
<\/span>Step 2: Install a Container Runtime<\/strong><\/span><\/h2>\n\n\n\nKubernetes needs a container runtime that implements the Container Runtime Interface (CRI). containerd<\/strong> is the current standard choice.<\/p>\n\n\n\nsudo apt update<\/pre>\nsudo apt install -y containerd<\/pre>\n\n\n\n <\/figure>\n\n\n\nGenerate and apply the default configuration:<\/p>\n\n\n\n
sudo mkdir -p \/etc\/containerd<\/pre>\ncontainerd config default | sudo tee \/etc\/containerd\/config.toml<\/pre>\n\n\n\n <\/figure>\n\n\n\nThen enable systemd cgroup management (required for kubelet compatibility):<\/p>\n\n\n\n
sudo sed -i 's\/SystemdCgroup = false\/SystemdCgroup = true\/' \/etc\/containerd\/config.toml<\/pre>\nsudo systemctl restart containerd<\/pre>\nsudo systemctl enable containerd<\/pre>\n\n\n\n <\/figure>\n\n\n\n<\/span>Step 3: Install kubeadm, kubelet, and kubectl<\/strong><\/span><\/h2>\n\n\n\nBefore adding the official Kubernetes APT repository, let\u2019s install some dependencies:<\/p>\n\n\n\n
sudo apt install -y apt-transport-https ca-certificates curl gpg<\/pre>\n\n\n\n <\/figure>\n\n\n\nNow, add the official Kubernetes APT repository and install the tooling:<\/p>\n\n\n\n
curl -fsSL https:\/\/pkgs.k8s.io\/core:\/stable:\/v1.36\/deb\/Release.key | \\ sudo gpg --dearmor -o \/etc\/apt\/keyrings\/kubernetes-apt-keyring.gpg<\/pre>\necho '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<\/pre>\n\n\n\n <\/figure>\n\n\n\nFinally, install kubeadm, kubelet, and kubectl as below:<\/p>\n\n\n\n
sudo apt update<\/pre>\nsudo apt install -y kubelet kubeadm kubectl<\/pre>\n\n\n\n <\/figure>\n\n\n\napt-mark hold prevents these packages from being upgraded automatically during routine apt upgrade runs. Kubernetes upgrades should always be deliberate, one minor version at a time.<\/p>\n\n\n\n
sudo apt-mark hold kubelet kubeadm kubectl<\/pre>\n\n\n\n <\/figure>\n\n\n\n<\/span>Step 4: Initialize the Control Plane<\/strong><\/span><\/h2>\n\n\n\nRun this step only on your designated control plane node:<\/p>\n\n\n\n
sudo kubeadm init --pod-network-cidr=192.168.0.0\/16<\/pre>\n\n\n\n <\/figure>\n\n\n\nThe --pod-network-cidr<\/code> value must match what your chosen CNI plugin (Calico, in this guide) expects. <\/p>\n\n\n\n <\/figure>\n\n\n\nThis step takes a few minutes and, on success, prints a kubeadm join command containing a token; save this immediately, since the token expires after 24 hours.<\/p>\n\n\n\n
<\/span>Configure kubectl Access<\/strong><\/span><\/h3>\n\n\n\nkubeadm init generates the cluster’s admin credentials in a root-owned file, so this copies them into your user’s home directory and fixes ownership, letting you run kubectl commands without sudo every time.<\/p>\n\n\n\n
mkdir -p $HOME\/.kube<\/pre>\nsudo cp -i \/etc\/kubernetes\/admin.conf $HOME\/.kube\/config<\/pre>\nsudo chown $(id -u):$(id -g) $HOME\/.kube\/config<\/pre>\n\n\n\n <\/figure>\n\n\n\nVerify the control plane is responding:<\/p>\n\n\n\n
kubectl get nodes<\/pre>\n\n\n\n <\/figure>\n\n\n\nThe control plane node will show as NotReady at this point. This is expected until the Calico CNI plugin is installed successfully.<\/p>\n\n\n\n
<\/span>Step 5: Install a Pod Network Add-on (Calico)<\/strong><\/span><\/h2>\n\n\n\nKubernetes doesn’t ship with built-in pod networking; you need a CNI plugin. Calico 3.32.1 is a current release tested with Kubernetes 1.34, 1.35, and 1.36, making it a suitable choice for this guide.<\/p>\n\n\n\n
kubectl apply -f https:\/\/raw.githubusercontent.com\/projectcalico\/calico\/v3.32.1\/manifests\/calico.yaml<\/pre>\n\n\n\nGive it a minute, then verify:<\/p>\n\n\n\n
kubectl get pods -n kube-system<\/pre>\n\n\n\n <\/figure>\n\n\n\nYour control plane node should now show Ready after Calico installation.<\/p>\n\n\n\n
Note:<\/strong> This guide uses the manifest-based installation because it is straightforward for learning and small clusters. For production environments, Project Calico recommends the operator-based installation, which provides easier lifecycle management and upgrades.<\/p>\n\n\n\n<\/span>Step 6: Join Worker Nodes to the Cluster<\/strong><\/span><\/h2>\n\n\n\nOn each worker node, run the join command you saved from Step 4:<\/p>\n\n\n\n
sudo kubeadm join <control-plane-ip>:6443 --token <token> \\ --discovery-token-ca-cert-hash sha256:<hash><\/pre>\n\n\n\n <\/figure>\n\n\n\nIf your token has expired, generate a new one from the control plane:<\/p>\n\n\n\n
kubeadm token create --print-join-command<\/pre>\n\n\n\nBack on the control plane, confirm all workers have joined successfully:<\/p>\n\n\n\n
kubectl get nodes<\/pre>\n\n\n\n <\/figure>\n\n\n\nAll nodes should eventually show Ready once the CNI has propagated to them.<\/p>\n\n\n\n
<\/span>Step 7: Deploy a Test Workload<\/strong><\/span><\/h2>\n\n\n\nConfirm the cluster is fully functional by deploying something real:<\/p>\n\n\n\n
kubectl create deployment nginx --image=nginx --replicas=3<\/pre>\nkubectl expose deployment nginx \\ --type=NodePort \\ --port=80<\/pre>\nkubectl get deployments<\/pre>\nkubectl get pods -o wide<\/pre>\nkubectl get services<\/pre>\n\n\n\n <\/figure>\n\n\n\nThe kubectl expose command creates a NodePort Service that exposes the Nginx deployment outside the cluster. After the pods are running, note the assigned NodePort from kubectl get services and test connectivity from any system that can reach a worker node.<\/p>\n\n\n\n
Use the assigned NodePort to verify that the application is accessible from outside the cluster:<\/p>\n\n\n\n
curl http:\/\/<worker-node-ip>:<node-port><\/pre>\n\n\n\n <\/figure>\n\n\n\nIf the default Nginx page is returned, the deployment, networking, and Service are functioning correctly.<\/p>\n\n\n\n
<\/span>Adding GPU Support for ML\/AI Workloads<\/strong><\/span><\/h2>\n\n\n\nIf you’re running machine learning inference or training workloads, GPU-enabled worker nodes need extra setup beyond a standard join. On any worker node built on a GPU-dedicated server<\/strong>, install the appropriate NVIDIA drivers first, then deploy the NVIDIA device plugin so Kubernetes can schedule GPU resources:<\/p>\n\n\n\nkubectl apply -f https:\/\/raw.githubusercontent.com\/NVIDIA\/k8s-device-plugin\/main\/nvidia-device-plugin.yml<\/pre>\n\n\n\n <\/figure>\n\n\n\nOnce deployed, GPU resources become schedulable in your pod specs via resources.limits.”nvidia.com\/gpu”, letting you target GPU-equipped nodes specifically for ML workloads while keeping general-purpose services on standard nodes.<\/p>\n\n\n\n
<\/span>Troubleshooting Common Cluster Build Issues<\/strong><\/span><\/h2>\n\n\n\n\nNode stuck in NotReady<\/strong>: Almost always means the CNI plugin isn’t fully deployed yet. Check with kubectl get pods -n kube-system and look for CrashLoopBackOff on Calico pods.<\/li>\n\n\n\nkubeadm join fails with a certificate error: <\/strong>Your join token or CA cert hash has likely expired. Generate a fresh one with kubeadm token create –print-join-command on the control plane.<\/li>\n\n\n\nPods stuck in Pending<\/strong>: Usually a resource constraint or a taint on your control plane node prevents scheduling. Check with kubectl describe pod <pod-name> for the exact reason.<\/li>\n\n\n\nkubelet won’t start<\/strong>: Check that swap is actually disabled (free -h should show 0 under swap) and review logs with journalctl -u kubelet -f.<\/li>\n<\/ul>\n\n\n\n<\/span>Conclusion<\/strong><\/span><\/h2>\n\n\n\nBuilding a Kubernetes cluster with kubeadm comes down to a consistent sequence: prepare every node identically, install a container runtime, bootstrap the control plane, install pod networking, then join your workers one at a time. The tooling handles most of the complexity for you; the parts that actually require judgment are your infrastructure choices up front, whether that’s picking reliable dedicated server hosting<\/strong> for consistent performance or provisioning a GPU dedicated server<\/strong> for ML workloads, and your operational discipline afterward around etcd backups, RBAC, and high availability. Get those right, and the cluster itself becomes the easy part.<\/p>\n\n\n\n<\/span>FAQs<\/strong><\/span><\/h2>\n\n\n\n1. How many nodes do I need to build a Kubernetes cluster?<\/strong><\/p>\n\n\n\nA minimum of two: one control plane node and one worker node. For production use, most teams run at least one control plane node and two or more worker nodes, and ideally multiple control plane nodes for high availability.<\/p>\n\n\n\n
2. Why does Kubernetes require swap to be disabled?<\/strong><\/p>\n\n\n\nKubelet is designed to manage memory resources predictably, and swap interferes with its ability to enforce memory limits and evict pods correctly under pressure. Because of this, kubeadm init and kubeadm join will fail outright if swap is still enabled.<\/p>\n\n\n\n
3. What’s the difference between kubeadm, kubelet, and kubectl?<\/strong><\/p>\n\n\n\nkubeadm is the tool used to bootstrap and join cluster nodes. kubelet is the agent that runs on every node and actually manages containers based on instructions from the control plane. kubectl is the command-line client you use to interact with and manage the cluster once it’s running.<\/p>\n\n\n\n
4. Why do I need a CNI plugin like Calico after running kubeadm init?<\/strong><\/p>\n\n\n\nKubernetes doesn’t include built-in pod-to-pod networking; that’s intentionally left to a separate Container Network Interface (CNI) plugin. Without one installed, nodes will stay stuck in a NotReady state indefinitely, since the cluster has no way to route traffic between pods.<\/p>\n\n\n\n
5. Why do my worker nodes fail to join or stay NotReady after joining?<\/strong><\/p>\n\n\n\nThe two most common causes are an expired kubeadm join token (tokens expire after 24 hours; generate a new one with kubeadm token create –print-join-command) and a firewall blocking required ports between nodes, such as Ubuntu’s ufw blocking port 6443 or the kubelet port 10250.<\/p>\n","protected":false},"excerpt":{"rendered":"Kubernetes has become the default way to run containerized applications at scale, but building a Kubernetes cluster from…","protected":false},"author":5,"featured_media":8828,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"csco_display_header_overlay":false,"csco_singular_sidebar":"","csco_page_header_type":"","footnotes":""},"categories":[1],"tags":[],"class_list":["post-8784","post","type-post","status-publish","format-standard","has-post-thumbnail","category-geneal","cs-entry"],"aioseo_notices":[],"aioseo_head":"\n\t\t\n\t \n\t \n\t \n\t \n\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t \n\t\t