$linuxjunkies
>

Build a Kubernetes Cluster with kubeadm

Bootstrap a production Kubernetes cluster with kubeadm: control-plane init, Calico or Cilium CNI, worker joins, kubelet hardening, and safe minor-version upgrades.

AdvancedUbuntuDebianFedoraArch14 min readUpdated June 7, 2026

Before you start

  • Root or passwordless sudo access on all nodes
  • Static IP addresses or stable DNS names for each node
  • Firewall rules or security groups permitting Kubernetes inter-node ports (6443, 2379-2380, 10250-10259)
  • Linux kernel 5.4+ if using Cilium; 4.x is sufficient for Calico

kubeadm is the official, supported way to bootstrap a production-grade Kubernetes cluster on your own infrastructure. It handles certificate generation, etcd setup, and control-plane Pod manifests so you can focus on architecture choices rather than plumbing. This guide walks from bare metal (or VMs) through a working multi-node cluster, CNI installation, and basic hardening — finishing with an upgrade and a troubleshooting reference.

Prerequisites and Node Planning

You need at least two machines: one control-plane node and one worker. Every node must meet these requirements:

  • 2 vCPU / 2 GB RAM minimum for the control plane (4 GB recommended); 1 vCPU / 1 GB RAM minimum per worker.
  • Unique hostname, MAC address, and product_uuid (sudo cat /sys/class/dmi/id/product_uuid).
  • Full network connectivity between nodes on the ports Kubernetes uses (6443, 2379–2380, 10250–10259).
  • Swap disabledkubelet 1.28 and earlier refuse to start with swap active unless you explicitly opt in.
  • A container runtime that implements CRI: containerd is the standard choice today.

Step 1 — Prepare Every Node

Disable swap

sudo swapoff -a
# Comment out any swap entries in /etc/fstab to survive reboots
sudo sed -i '/ swap / s/^/#/' /etc/fstab

Load required kernel modules

cat <

Set sysctl parameters

cat <

Install containerd

Debian/Ubuntu:

sudo apt-get update
sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
# Enable systemd cgroup driver — required for kubelet compatibility
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd

Fedora/RHEL family:

sudo dnf install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd

Step 2 — Install kubeadm, kubelet, and kubectl

Kubernetes publishes its own package repositories. Use the versioned repo path (replace v1.30 with your target minor version).

Debian/Ubuntu:

sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/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.30/deb/ /' | \
  sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

Fedora/RHEL family:

cat <

Arch (AUR):

yay -S kubeadm kubelet kubectl-bin
sudo systemctl enable kubelet

Step 3 — Initialize the Control Plane

Run this only on the control-plane node. Choose a Pod CIDR that does not overlap your node network; the example below suits Calico. For Cilium, any non-overlapping CIDR works.

sudo kubeadm init \
  --pod-network-cidr=192.168.0.0/16 \
  --control-plane-endpoint="$(hostname -f):6443" \
  --upload-certs

On success, kubeadm prints a kubeadm join command with a token and CA hash. Save it. Then configure kubectl for your user:

mkdir -p $HOME/.kube
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Step 4 — Install a CNI Plugin

Nodes stay in NotReady until a CNI plugin is running. Pick one and apply it to the control plane before joining workers.

Calico (stable, widely deployed)

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/custom-resources.yaml

Confirm Calico pods reach Running before proceeding:

kubectl get pods -n calico-system --watch

Cilium (eBPF-based, richer observability)

CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --fail --remote-name-all \
  "https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz"
sudo tar xzvf cilium-linux-amd64.tar.gz -C /usr/local/bin
cilium install --version 1.15.5
cilium status --wait

Cilium requires Linux kernel 5.4 or later for its eBPF dataplane. Check with uname -r before choosing it.

Step 5 — Join Worker Nodes

On each worker, run the join command printed by kubeadm init. It looks like:

sudo kubeadm join 203.0.113.10:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:<hash>

Tokens expire after 24 hours. Generate a new one on the control plane if needed:

kubeadm token create --print-join-command

Verify all nodes are Ready from the control plane:

kubectl get nodes -o wide

Step 6 — Harden kubelet

The default kubelet configuration is permissive. Apply these settings by patching the kubelet config on every node.

# View current kubelet config
kubectl get --raw /api/v1/nodes/$(hostname)/proxy/configz | python3 -m json.tool

Create a KubeletConfiguration drop-in. Edit /etc/kubernetes/kubelet-hardening.yaml on each node:

cat <

Point kubelet at this config by adding --config=/etc/kubernetes/kubelet-hardening.yaml to the kubelet systemd drop-in, then restart:

sudo mkdir -p /etc/systemd/system/kubelet.service.d
cat <

Step 7 — Upgrade the Cluster

Kubernetes supports upgrading one minor version at a time. Always upgrade the control plane first, then workers. This example moves from 1.30.x to 1.31.x.

Control-plane node

# Update the repo to v1.31, then:
sudo apt-get update && sudo apt-get install -y kubeadm=1.31.0-1.1
sudo kubeadm upgrade plan
sudo kubeadm upgrade apply v1.31.0
sudo apt-get install -y kubelet=1.31.0-1.1 kubectl=1.31.0-1.1
sudo systemctl daemon-reload && sudo systemctl restart kubelet

Each worker node

# From the control plane, drain the node first:
kubectl drain worker-01 --ignore-daemonsets --delete-emptydir-data

# Then on the worker itself:
sudo apt-get update && sudo apt-get install -y kubeadm=1.31.0-1.1
sudo kubeadm upgrade node
sudo apt-get install -y kubelet=1.31.0-1.1 kubectl=1.31.0-1.1
sudo systemctl daemon-reload && sudo systemctl restart kubelet

# Back on the control plane, uncordon:
kubectl uncordon worker-01

Verification

kubectl get nodes
kubectl get pods -A
kubectl cluster-info

All nodes should show Ready and all system pods Running. Deploy a quick smoke test:

kubectl run nginx --image=nginx --port=80
kubectl expose pod nginx --type=NodePort --port=80
kubectl get svc nginx

Troubleshooting

  • Node stays NotReady: CNI is usually the cause. Check kubectl describe node <name> and kubectl get pods -n kube-system. If CNI pods are crashing, verify the pod CIDR you gave kubeadm init matches what the CNI manifests expect.
  • kubelet fails to start: Run sudo journalctl -xeu kubelet. The most common causes are swap still active, wrong cgroup driver (containerd must use SystemdCgroup = true), or a misconfigured kubelet config file.
  • kubeadm init fails on preflight: Read the preflight errors carefully — they are specific. Re-run with --ignore-preflight-errors=all only for non-production testing.
  • Expired token on join: Generate a fresh token with kubeadm token create --print-join-command.
  • Certificate errors after upgrade: If you use --upload-certs for HA control planes, the certificate key expires in 2 hours. Re-upload with kubeadm init phase upload-certs --upload-certs.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

Can I run a single-node cluster with kubeadm for development?
Yes. After kubeadm init, remove the control-plane taint with: kubectl taint nodes --all node-role.kubernetes.io/control-plane-. This allows workloads to schedule on the control-plane node.
Calico or Cilium — which should I choose?
Calico is the safer default: it works on kernels 4.x and up, has a long production track record, and is simpler to troubleshoot. Choose Cilium if you want eBPF-based networking, transparent encryption, or deeper observability via Hubble, and your nodes run kernel 5.4 or later.
How do I add a second control-plane node for high availability?
Run kubeadm init with --upload-certs and --control-plane-endpoint pointing at a load balancer VIP. Use the --control-plane flag on the join command for additional control-plane nodes. The certificate key printed at init expires in two hours.
Why does kubeadm refuse to run if swap is on?
The kubelet's memory management and QoS guarantees assume swap is absent. Since Kubernetes 1.28 there is experimental swap support you can opt into via KubeletConfiguration, but disabling swap remains the supported path for production clusters.
How do I completely reset a node and start over?
Run sudo kubeadm reset on the node, then manually clean up CNI config files in /etc/cni/net.d and any iptables or nftables rules left by the CNI plugin. On the control plane, remove the node with kubectl delete node <name> before resetting it.

Related guides