How to Build a Kubernetes Cluster: Step-by-Step Guide

How to Build a Kubernetes Cluster

Kubernetes has become the default way to run containerized applications at scale, but building a cluster from scratch is still where a lot of engineers get stuck the first time around, not because the concepts are hard, but because the tooling assumes you already know which of the dozen configuration steps actually matter.

This guide walks through building a production-style Kubernetes cluster using kubeadm, the official bootstrapping tool, covering infrastructure planning, control plane setup, worker node configuration, networking, and the operational details most tutorials skip.

We’ll be building a cluster with one control plane node and multiple worker nodes, using containerd as the container runtime and Calico for pod networking, running current Kubernetes 1.36.

Choosing Your Infrastructure Before You Start

Before writing a single command, decide where your cluster will actually run. This decision affects performance, cost, and long-term flexibility more than almost anything else in this guide.

Managed Kubernetes services (EKS, GKE, AKS) handle the control plane for you, but they come with a real per-cluster cost and less control over the underlying hardware.

For teams that want full control, predictable pricing, and the ability to tune performance at the hardware level, building your cluster on dedicated server hosting is a common and cost-effective alternative. You get consistent CPU and network performance without the noisy-neighbor issues that can come with shared cloud instances, and no per-cluster management fee eating into your budget.

If your workloads involve machine learning, model training, or GPU-accelerated inference alongside your regular services, running at least some of your worker nodes on a GPU dedicated server is worth planning for from the start. 

How to Build a Kubernetes Cluster: Step-by-Step Guide

To build a Kubernetes cluster on Ubuntu 24.04, start by disabling swap and loading required kernel modules (overlay, br_netfilter) on every node, then install containerd as the container runtime and configure it with SystemdCgroup = true. 

Then, add the official Kubernetes APT repository and install kubeadm, kubelet, and kubectl on all nodes. On the control plane node, run sudo kubeadm init --pod-network-cidr=192.168.0.0/16 to bootstrap the cluster, configure kubectl access, and install a CNI plugin like Calico for pod networking. 

Finally, join each worker node to the cluster using the kubeadm join command generated during initialization, and verify all nodes show Ready with kubectl get nodes. The entire process typically takes 20–30 minutes across a small cluster.

Minimum Requirements Per Node

  • Control plane node: 2 CPUs, 2 GB RAM minimum (4 GB+ recommended for production)
  • Worker nodes: 2 CPUs, 2 GB RAM minimum, scaled up based on workload
  • A Linux distribution supported by kubeadm (Ubuntu 24.04/26.04 LTS)
  • Full network connectivity between all nodes, with unique hostnames, MAC addresses, and product_uuid for each
  • Swap disabled on every node; this is a hard requirement for kubelet to function correctly

Step 1: Prepare Every Node (Control Plane and Workers)

Run these steps identically on every machine that will join the cluster.

Disable Swap

Kubelet 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.

sudo swapoff -a

sudo sed -i '/ swap / s/^/#/' /etc/fstab

Terminal session showing commands to disable swap: 'sudo swapoff -a' (prompts for password), then editing /etc/fstab with 'sudo sed -i "/ swap / s/^#//" /etc/fstab' to uncomment the swap entry for on-boot behavior.

Load Required Kernel Modules

The 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.

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF
sudo modprobe overlay
sudo modprobe br_netfilter
Terminal commands showing creating /etc/modules-load.d/k8s.conf with overlay and br_netfilter lines (EOF block).

Configure Required sysctl Parameters.

These settings ensure that iptables processes traffic crossing network bridges and that the kernel forwards packets between pods, which Kubernetes networking depends on.

cat <<EOF | sudo tee /etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-iptables  = 1net.bridge.bridge-nf-call-ip6tables = 1net.ipv4.ip_forward                 = 1EOF
Terminal showing a heredoc writing Kubernetes network settings to /etc/sysctl.d/k8s.conf: enable bridge netfilter for IPv4/IPv6 and IPv4 forwarding (values = 1).

These settings ensure traffic crossing network bridges is correctly processed by iptables, which Kubernetes networking depends on.

Step 2: Install a Container Runtime

Kubernetes needs a container runtime that implements the Container Runtime Interface (CRI). containerd is the current standard choice.

sudo apt update
sudo apt install -y containerd
Terminal session: runs 'sudo apt update' to refresh package lists, then 'sudo apt install -y containerd' to install containerd; progress lines shown.

Generate and apply the default configuration:

sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
Shell session showing commands to create /etc/containerd and write its default config to /etc/containerd/config.toml (containerd config default | sudo tee /etc/containerd/config.toml) with sample config output

Then enable systemd cgroup management (required for kubelet compatibility):

sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd
Terminal showing commands to enable SystemdGroup: sed to replace 'SystemdGroup = false' with 'SystemdGroup = true' in /etc/containerd/config.toml, then restart and enable containerd with systemctl commands.

Step 3: Install kubeadm, kubelet, and kubectl

Before adding the official Kubernetes APT repository, let’s install some dependencies:

sudo apt install -y apt-transport-https ca-certificates curl gpg
Terminal command: 'sudo apt install -y apt-transport-https ca-certificates curl ...' shown during package upgrade on Ubuntu; progress lines in green.

Now, add the official Kubernetes APT repository and install the tooling:

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key | \  sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
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
Ubuntu terminal showing commands to add the Kubernetes APT repository: fetch the key, convert to gpg, and add the deb source

Finally, install kubeadm, kubelet, and kubectl as below:

sudo apt update
sudo apt install -y kubelet kubeadm kubectl
Terminal session showing Ubuntu update and Kubernetes component installation: commands "sudo apt update" and "sudo apt install -y kubelet kubeadm kubectl" with package download messages and progress.

apt-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.

sudo apt-mark hold kubelet kubeadm kubectl
Terminal command: sudo apt-mark hold kubelet kubeadm kubectl, with each package reported as set on hold.

Step 4: Initialize the Control Plane

Run this step only on your designated control plane node:

sudo kubeadm init --pod-network-cidr=192.168.0.0/16
Terminal window showing the command 'sudo kubeadm init --pod-network-cidr=192.168.0.0/16' and the init output, including Kubernetes version v1.36.3.

The --pod-network-cidr value must match what your chosen CNI plugin (Calico, in this guide) expects. 

Terminal log showing Kubernetes kubelet startup messages, including manifest creation and starting the kubelet.

This 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.

Configure kubectl Access

kubeadm 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.

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Terminal commands to configure Kubernetes: create ~/.kube, copy /etc/kubernetes/admin.conf to ~/.kube/config with prompt, and set ownership to current user:group

Verify the control plane is responding:

kubectl get nodes
Terminal showing kubectl get nodes output: two nodes — controlplane and worker1 — both Ready; roles: control-plane and ; age 2d3h; version v1.29.2.

The control plane node will show as Ready at this point.

Step 5: Install a Pod Network Add-on (Calico)

Kubernetes doesn’t ship with built-in pod networking; you need a CNI plugin. Calico is a solid, widely used default:

kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/calico.yaml
Shell terminal with a user@host prompt and a kubectl apply command loading Calico YAML from GitHub.

Give it a minute, then verify:

kubectl get pods -n kube-system
Terminal: kubectl get pods -n kube-system listing pods with NAME, READY, STATUS, RESTARTS, AGE; all show READY 1/1 and Status Running, e.g., coredns, etcd, kube-apiserver-minikube, storage-provisioner, age about 2d3h.

Your control plane node should now show Ready.

Step 6: Join Worker Nodes to the Cluster

On each worker node, run the join command you saved from Step 4:

sudo kubeadm join <control-plane-ip>:6443 --token <token> \  --discovery-token-ca-cert-hash sha256:<hash>
Terminal window showing a kubadm join command to add a node to a Kubernetes cluster; the node reports it joined the cluster and a certificate signing request was sent.

If your token has expired, generate a new one from the control plane:

kubeadm token create --print-join-command

Back on the control plane, confirm all workers have joined successfully:

kubectl get nodes
Terminal output: kubectl get nodes showing two nodes (controlplane and worker1) both Ready; control-plane on controlplane, AGE 2d3h, version v1.29.2.

All nodes should eventually show Ready once the CNI has propagated to them.

Step 7: Deploy a Test Workload

Confirm the cluster is fully functional by deploying something real:

kubectl create deployment nginx --image=nginx --replicas=3
kubectl get deployment
Terminal showing Kubernetes commands: create deployment nginx with 3 replicas, then kubectl get deployments; output lists nginx as READY 3/3, UP-TO-DATE 3, AVAILABLE 3, AGE 4s.

If pods are distributed across your worker nodes and reachable through the exposed service, your cluster is working correctly.

Adding GPU Support for ML/AI Workloads

If 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, install the appropriate NVIDIA drivers first, then deploy the NVIDIA device plugin so Kubernetes can schedule GPU resources:

kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/main/nvidia-device-plugin.yml
Terminal shows kubectl apply for NVIDIA device plugin YAML; then serviceaccount, clusterrole, clusterrolebinding, and daemonset created.

Once 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.

Troubleshooting Common Cluster Build Issues

  • Node stuck in NotReady: 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.
  • kubeadm join fails with a certificate error: 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.
  • Pods stuck in Pending: 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.
  • kubelet won’t start: Check that swap is actually disabled (free -h should show 0 under swap) and review logs with journalctl -u kubelet -f.

Conclusion

Building 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 for consistent performance or provisioning a GPU dedicated server 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.

Leave a Reply

Your email address will not be published. Required fields are marked *