← Back to DevBytes

How to Set Up a Kubernetes Cluster with kubeadm

Introduction to kubeadm

kubeadm is the official Kubernetes tool designed to bootstrap a minimally viable, best-practice Kubernetes cluster. It handles the heavy lifting of generating certificates, configuring control plane components, and wiring up the networking layer—without being a full-blown production installer. Think of it as the foundation upon which you build your cluster: it creates a conformant Kubernetes environment that passes all Kubernetes conformance tests, leaving you in control of add-ons like networking, monitoring, and ingress.

Why kubeadm Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before kubeadm, setting up a Kubernetes cluster meant manually crafting certificates, kubeconfig files, and systemd unit files for each component (kube-apiserver, kube-scheduler, kube-controller-manager, etc.). This was error-prone and time-consuming. kubeadm solves this by:

kubeadm is ideal for development clusters, CI/CD environments, learning labs, and even production when augmented with proper lifecycle management (like Cluster API or kubeadm-based automation).

Prerequisites

Before you begin, ensure the following on each node (both control plane and worker nodes):

Step 1: Prepare All Nodes

Perform the following steps on every node in your cluster. We'll use containerd as the container runtime.

1.1 Disable Swap

# Turn off swap immediately
sudo swapoff -a

# Comment out any swap entries in /etc/fstab to make it permanent
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

# Verify swap is disabled
free -h
# The Swap line should show 0

1.2 Load Kernel Modules and Set Sysctl Parameters

Kubernetes requires certain kernel modules for networking and container management.

# Load overlay and br_netfilter modules
cat <

1.3 Install containerd

We'll install containerd from the official Docker/containerd repositories on Ubuntu 22.04. Adjust package names for your distribution.

# Install dependencies
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# Add Docker's official GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Add the repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install containerd
sudo apt-get update
sudo apt-get install -y containerd.io

1.4 Configure containerd with systemd cgroup driver

Kubernetes recommends using the systemd cgroup driver for consistent resource management.

# Generate default containerd configuration
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml

# Enable systemd cgroup driver
# This replaces "SystemdCgroup = false" with "SystemdCgroup = true" in the config
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# Restart containerd
sudo systemctl restart containerd
sudo systemctl enable containerd

1.5 Install kubeadm, kubelet, and kubectl

These packages are distributed via Google's apt repository. Install them on all nodes.

# Add Kubernetes apt repository
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/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.28/deb/ /' | \
  sudo tee /etc/apt/sources.list.d/kubernetes.list

# Install kubeadm, kubelet, and kubectl
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl

# Hold them at the installed version to prevent accidental upgrades
sudo apt-mark hold kubelet kubeadm kubectl

Note: Replace v1.28 with your desired Kubernetes version. The same repository structure works for 1.27, 1.29, etc.

Step 2: Initialize the Control Plane Node

Choose one node as your control plane. This node will run the API server, scheduler, controller manager, and etcd. Run the following command only on the control plane node.

2.1 Run kubeadm init

# Initialize the control plane
# --pod-network-cidr is required for most CNI plugins (Calico, Flannel, etc.)
sudo kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --kubernetes-version=1.28.7 \
  --control-plane-endpoint=CONTROL_PLANE_IP:6443

# Replace CONTROL_PLANE_IP with the actual IP address of this node
# Example: --control-plane-endpoint=192.168.1.10:6443

The --pod-network-cidr flag defines the IP range for pods. Flannel uses 10.244.0.0/16 by default; Calico can work with other ranges. The --control-plane-endpoint flag sets a stable address for the API server, which is critical for high availability setups later.

If successful, you'll see output similar to:

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you must 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

You can now 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:...

2.2 Configure kubectl Access

Copy the admin kubeconfig to your home directory so you can interact with the cluster 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

# Verify the cluster is reachable
kubectl cluster-info

# Check node status (only the control plane node should appear for now)
kubectl get nodes

At this point, the control plane node will show a NotReady status because no CNI (Container Network Interface) plugin has been installed yet. Pods cannot communicate across nodes without a CNI.

Step 3: Install a CNI Plugin

The cluster needs a CNI plugin for pod networking. We'll use Calico as an example—it's robust, supports network policies, and works well in production.

3.1 Install Calico

# Download the Calico manifest and apply it
# This installs Calico v3.26 with the default pod network CIDR
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml

# Wait for the calico-node pods to become Ready
kubectl wait --namespace kube-system \
  --for=condition=ready pod \
  --selector=k8s-app=calico-node \
  --timeout=300s

After a minute or so, your control plane node should transition to Ready:

kubectl get nodes
# Output:
# NAME          STATUS   ROLES           AGE   VERSION
# cp-node       Ready    control-plane   5m    v1.28.7

3.2 Alternative CNI: Flannel

If you prefer Flannel, apply its manifest instead:

kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

Important: Flannel expects --pod-network-cidr=10.244.0.0/16. If you used a different CIDR, edit the manifest before applying.

Step 4: Join Worker Nodes

On each worker node, run the kubeadm join command that was printed during init. If you lost the command or the token expired, you can generate a new one on the control plane node.

4.1 Generate a New Join Token (if needed)

# On the control plane node, create a new token
sudo kubeadm token create --print-join-command

# This prints the full join command, including the discovery token hash
# Example output:
# kubeadm join 192.168.1.10:6443 --token n7q8x9.y4z5a6b7c8d9e0f1 \
#     --discovery-token-ca-cert-hash sha256:abc123...

4.2 Run the Join Command on Each Worker

# On the worker node, run the join command as root
sudo kubeadm join 192.168.1.10:6443 --token n7q8x9.y4z5a6b7c8d9e0f1 \
  --discovery-token-ca-cert-hash sha256:abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890abcd

# Output:
# This node has joined the cluster:
# * Certificate signing request was sent to apiserver and a new certificate was received.
# * Kubelet was informed the new secure connection details.
# Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

4.3 Verify the Cluster

# On the control plane node, list all nodes
kubectl get nodes

# Expected output (after CNI propagates):
# NAME          STATUS   ROLES           AGE   VERSION
# cp-node       Ready    control-plane   10m   v1.28.7
# worker-1      Ready              2m    v1.28.7
# worker-2      Ready              1m    v1.28.7

# Check all system pods
kubectl get pods -n kube-system

Step 5: Deploy a Test Application

Let's deploy a simple nginx deployment to verify everything works end-to-end.

# Create an nginx deployment
kubectl create deployment nginx --image=nginx:latest --replicas=2

# Expose it as a NodePort service
kubectl expose deployment nginx --port=80 --type=NodePort

# Get the service details
kubectl get service nginx

# Output:
# NAME    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
# nginx   NodePort   10.96.123.45            80:30080/TCP   5s

# Test the service using any node's IP and the NodePort
curl http://WORKER_IP:30080
# You should see the nginx welcome page

Best Practices

  • Use a Dedicated Control Plane Node – Avoid scheduling workloads on the control plane node in production. Remove the taint if needed for small dev clusters: kubectl taint nodes cp-node node-role.kubernetes.io/control-plane:NoSchedule-
  • Always Specify Pod Network CIDR – The --pod-network-cidr flag during kubeadm init ensures your CNI plugin can allocate IPs correctly.
  • Use a Stable Control Plane Endpoint – For single-node control planes, use the node's IP. For multi-node (HA) setups, use a load balancer DNS name or virtual IP.
  • Automate with Configuration Files – Instead of passing flags, create a kubeadm-config.yaml file for reproducible cluster bootstrapping. Use kubeadm config print init-defaults to generate a template.
  • Back Up /etc/kubernetes – This directory contains all certificates, kubeconfig files, and static pod manifests. Back it up immediately after initialization.
  • Plan for Upgrades – kubeadm supports versioned upgrades. Always upgrade the control plane first with kubeadm upgrade apply, then each worker with kubeadm upgrade node.
  • Secure the Join Token – Tokens expire after 24 hours by default. Generate new ones as needed and never expose them in version control.
  • Use containerd with systemd cgroup – This combination provides the most stable experience with Kubernetes cgroup management and avoids conflicts with systemd.

Advanced: Using a kubeadm Configuration File

For production or repeatable setups, generate a configuration file instead of relying solely on CLI flags. This lets you customize the API server, controller manager, scheduler, and networking settings declaratively.

# Generate a default init configuration
kubeadm config print init-defaults > kubeadm-config.yaml

# Edit the file to customize:
# - advertiseAddress: your control plane IP
# - networking.podSubnet: your pod CIDR
# - nodeRegistration.criSocket: unix:///var/run/containerd/containerd.sock
# - apiServer.extraArgs for audit logging, etc.

# Then initialize with the config file
sudo kubeadm init --config=kubeadm-config.yaml

Troubleshooting Common Issues

Issue: Node stays in NotReady state after joining

This is almost always a CNI issue. Verify that the CNI pods are running on the node:

kubectl get pods -n kube-system -o wide | grep calico

If pods are stuck in Pending or Error, check CNI logs and ensure the pod network CIDR matches what the CNI expects.

Issue: kubeadm init fails with port errors

Ensure ports 6443 (API server), 10250 (kubelet), and 2379-2380 (etcd) are not in use. Common culprits include leftover kubelet processes or another control plane component.

sudo netstat -tulpn | grep -E '6443|10250|2379|2380'
# Clean up with: sudo kubeadm reset

Issue: containerd fails to pull images

Ensure containerd is configured correctly and that the SystemdCgroup setting is true. Restart containerd after changes:

sudo systemctl restart containerd
sudo crictl pull k8s.gcr.io/pause:3.9  # test with crictl

Conclusion

Setting up a Kubernetes cluster with kubeadm demystifies what was once a daunting task. By following the steps outlined—preparing nodes with containerd and kernel modules, initializing the control plane, installing a CNI plugin, and joining workers—you get a fully conformant cluster ready for workloads. kubeadm's design philosophy of keeping things simple yet extensible means you can start small and grow into production-grade deployments with proper configuration management, backups, and upgrade planning. Whether you're building a development sandbox or laying the groundwork for a production platform, kubeadm gives you a solid, community-vetted foundation to build upon.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles