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.
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 disabled — kubelet 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>andkubectl get pods -n kube-system. If CNI pods are crashing, verify the pod CIDR you gavekubeadm initmatches 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 useSystemdCgroup = 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=allonly 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-certsfor HA control planes, the certificate key expires in 2 hours. Re-upload withkubeadm init phase upload-certs --upload-certs.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.