--- title: "How to Install Kubernetes with kubeadm on Ubuntu 24.04" description: "Build a Kubernetes cluster on Ubuntu 24.04 with kubeadm and containerd: prepare the nodes, initialize the control plane, add Flannel and join worker nodes." category: "Docker Kubernetes" language: en source: https://cubepath.com/docs/docker-kubernetes/kubernetes-installation-with-kubeadm updated: 2026-09-25 --- # How to Install Kubernetes with kubeadm on Ubuntu 24.04 kubeadm is the upstream tool for bootstrapping a conformant Kubernetes cluster. It sets up the control plane components (API server, scheduler, controller manager and etcd) and generates the certificates and tokens that worker nodes need to join. In this tutorial you will build a cluster with one control plane node and one or more worker nodes on Ubuntu 24.04, using containerd as the container runtime and Flannel as the pod network. ## Prerequisites To follow this tutorial you need: - Two or more servers running Ubuntu 24.04 LTS, for example CubePath VPS instances: one will be the control plane, the rest will be workers. - At least 2 vCPUs and 2 GB of RAM on the control plane node, and 2 GB of RAM on each worker. kubeadm refuses to initialize a control plane with fewer than 2 CPUs. - A non-root user with `sudo` privileges on every node. - Full network connectivity between the nodes, and a unique hostname, MAC address and `product_uuid` on each one. Cloned VMs sometimes share these values. Throughout the guide, replace `control_plane_ip` with the IP address the control plane will advertise to the other nodes. If your servers have a private network, use the private IP. Steps 1 to 4 must be run on **every node**. Step 5 and later say explicitly which node to use. ## Step 1 - Preparing the nodes The kubelet refuses to start by default if swap is enabled, so turn it off now and remove it from `/etc/fstab` so it stays off after a reboot: ```bash sudo swapoff -a sudo sed -i '/\sswap\s/ s/^/#/' /etc/fstab ``` Confirm that no swap is active. The `Swap` line must show zeros: ```bash free -h ``` ```output total used free shared buff/cache available Mem: 3.8Gi 412Mi 2.9Gi 1.0Mi 735Mi 3.4Gi Swap: 0B 0B 0B ``` Kubernetes networking needs the `overlay` and `br_netfilter` kernel modules. Load them now and configure them to load at boot: ```bash cat < /dev/null ``` Ubuntu 24.04 uses cgroup v2 managed by systemd, and the kubelet uses the `systemd` cgroup driver by default. containerd must use the same driver, otherwise pods restart in a loop. Switch the runc option `SystemdCgroup` to `true`: ```bash sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml ``` Confirm the change and restart containerd: ```bash grep SystemdCgroup /etc/containerd/config.toml sudo systemctl restart containerd ``` ```output SystemdCgroup = true ``` Check that the service is running: ```bash systemctl status containerd --no-pager ``` The output must include `Active: active (running)`. ## Step 4 - Installing kubeadm, kubelet and kubectl Kubernetes packages are published at `pkgs.k8s.io`, with one repository per minor version. This guide uses `v1.35`. Check the current stable release at [kubernetes.io/releases](https://kubernetes.io/releases/) and replace the version in both commands below if a newer one is available. Install the packages needed to add the repository: ```bash sudo apt install -y apt-transport-https ca-certificates curl gpg ``` Download the repository signing key into `/etc/apt/keyrings`: ```bash sudo mkdir -p -m 755 /etc/apt/keyrings curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg ``` Add the repository: ```bash echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.35/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list ``` Install the three components and pin them, so a routine `apt upgrade` does not upgrade Kubernetes behind your back. Cluster upgrades must be done with `kubeadm upgrade`, one minor version at a time: ```bash sudo apt update sudo apt install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl ``` Enable the kubelet. It will restart every few seconds until `kubeadm init` or `kubeadm join` gives it a configuration; that is expected at this point: ```bash sudo systemctl enable --now kubelet ``` Verify the installed version: ```bash kubeadm version -o short ``` ```output v1.35.3 ``` ## Step 5 - Initializing the control plane Run this step **only on the control plane node**. Pull the control plane images first. This makes `kubeadm init` faster and shows registry problems early: ```bash sudo kubeadm config images pull ``` Initialize the cluster. `--pod-network-cidr` must match the network that Flannel uses by default (`10.244.0.0/16`), and `--apiserver-advertise-address` sets the IP that the other nodes will connect to: ```bash sudo kubeadm init \ --apiserver-advertise-address=control_plane_ip \ --pod-network-cidr=10.244.0.0/16 ``` After a minute or two the command finishes with a message like this: ```output Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to 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 ... Then you can join any number of worker nodes by running the following on each as root: kubeadm join 10.0.0.10:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:1234...cdef ``` Copy the `kubeadm join` command somewhere safe; you will need it in Step 7. Configure `kubectl` for your user by copying the admin kubeconfig: ```bash mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config ``` > Warning: `admin.conf` grants full administrator access to the cluster. Keep it private and never commit it to a repository. Check the node: ```bash kubectl get nodes ``` ```output NAME STATUS ROLES AGE VERSION control-plane NotReady control-plane 1m v1.35.3 ``` The node is `NotReady` because there is no pod network yet. ## Step 6 - Installing the Flannel pod network A CNI plugin gives every pod an IP address and routes traffic between pods on different nodes. Flannel is simple and works well for small and medium clusters. Install it from its official release manifest on the control plane node: ```bash kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml ``` > Note: Flannel uses the interface that holds the default route. If your nodes talk to each other over a second (private) interface, download `kube-flannel.yml` first, add `- --iface=your_private_interface` to the `args` of the `kube-flannel` container, and apply the edited file instead. Wait until the Flannel pods are running: ```bash kubectl get pods -n kube-flannel ``` ```output NAME READY STATUS RESTARTS AGE kube-flannel-ds-7xk2p 1/1 Running 0 45s ``` The node now becomes `Ready`, and the CoreDNS pods in `kube-system` start: ```bash kubectl get nodes kubectl get pods -n kube-system ``` ## Step 7 - Joining the worker nodes Run the `kubeadm join` command from Step 5 on **each worker node**, with `sudo`: ```bash sudo kubeadm join control_plane_ip:6443 --token your_token \ --discovery-token-ca-cert-hash sha256:your_hash ``` ```output This node has joined the cluster: * Certificate signing request was sent to apiserver and a response was received. * The Kubelet was informed of the new secure connection details. ``` Join tokens expire after 24 hours. If yours has expired, or you lost the command, generate a new one on the control plane: ```bash sudo kubeadm token create --print-join-command ``` Back on the control plane, check that all nodes are `Ready`. New workers take about a minute while Flannel starts on them: ```bash kubectl get nodes -o wide ``` ```output NAME STATUS ROLES AGE VERSION INTERNAL-IP control-plane Ready control-plane 12m v1.35.3 10.0.0.10 worker-1 Ready 2m v1.35.3 10.0.0.11 worker-2 Ready 1m v1.35.3 10.0.0.12 ``` ## Step 8 - Testing the cluster Deploy two Nginx replicas and expose them with a NodePort service to confirm that scheduling, pod networking and service routing all work: ```bash kubectl create deployment nginx --image=nginx --replicas=2 kubectl expose deployment nginx --port=80 --type=NodePort ``` Check where the pods were scheduled and which port was assigned: ```bash kubectl get pods -o wide kubectl get service nginx ``` ```output NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx NodePort 10.96.141.22 80:31234/TCP 20s ``` Send a request to any worker node on the NodePort (here `31234`): ```bash curl -I http://worker_node_ip:31234 ``` ```output HTTP/1.1 200 OK Server: nginx/1.29.1 ``` Test cluster DNS from a temporary pod. The service name must resolve to its ClusterIP: ```bash kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- nslookup nginx.default.svc.cluster.local ``` When everything works, remove the test resources: ```bash kubectl delete service nginx kubectl delete deployment nginx ``` ## Troubleshooting **Pods on different nodes cannot reach each other, or CoreDNS stays in `ContainerCreating`.** Check that UDP port 8472 is allowed between the nodes and that Flannel uses the right interface (see the note in Step 6). Inspect the Flannel logs with `kubectl logs -n kube-flannel -l app=flannel`. **Pods keep restarting and `kubectl get pods -n kube-system` shows `CrashLoopBackOff` for control plane components.** This is almost always a cgroup driver mismatch. Confirm that `SystemdCgroup = true` is set in `/etc/containerd/config.toml` and restart containerd. **`kubeadm init` fails a preflight check.** Read the message: typical causes are swap still enabled, fewer than 2 CPUs, or `/proc/sys/net/ipv4/ip_forward` not set to `1`. Fix the cause rather than skipping the check with `--ignore-preflight-errors`. **The kubelet does not start.** Read its log with `sudo journalctl -u kubelet -e`. **You need to start over on a node.** `kubeadm reset` removes what `kubeadm init` or `join` created. It does not clean CNI configuration or iptables rules, so remove those too before rejoining: ```bash sudo kubeadm reset -f sudo rm -rf /etc/cni/net.d $HOME/.kube/config ``` ## Conclusion You now have a working Kubernetes cluster built with kubeadm, containerd and Flannel, with worker nodes joined and verified by a test deployment. The packages are pinned, so upgrades happen only when you run `kubeadm upgrade` deliberately. As next steps, learn the core objects in [Kubernetes Pods, Services and Deployments](/docs/docker-kubernetes/kubernetes-pods-services-deployments), expose HTTP applications with an Ingress controller, and add persistent storage with a StorageClass before running stateful workloads.