Skip to main content
This guide walks you through installing kubeadm, kubelet, and kubectl on a control plane and a single worker node (node01), initializing the control plane, joining the worker, and deploying the Flannel pod network. Follow the steps in order on both the control plane and node01 where indicated. Contents
    1. Prepare kernel networking settings (both nodes)
    1. Add the Kubernetes apt repository (both nodes)
    1. Install kubeadm, kubelet, kubectl (pinned version) (both nodes)
    1. Before initialization: kubectl on control plane
    1. Initialize the control plane with kubeadm (control plane only)
    1. Join node01 to the cluster (node01 only)
    1. Install a pod network (Flannel) (control plane)
  • Summary and Troubleshooting
  • Links and references

1) Prepare kernel networking settings (both nodes)

Kubernetes requires bridged network traffic to be handled by iptables and IP forwarding to be enabled. Create a sysctl config file (for example /etc/sysctl.d/k8s.conf) with the required keys:
Apply the settings immediately (no reboot required):
You should see lines indicating the k8s settings were applied, e.g.:
Kubeadm requires swap to be disabled on all nodes. Disable swap now and make it persistent across reboots before running kubeadm init / kubeadm join:
Kubernetes needs an OCI runtime (for example containerd or Docker) installed and running on all nodes before initializing the control plane. One quick approach is to install and configure containerd with the systemd cgroup driver:
Confirm the runtime is active before running kubeadm init/join.

2) Add the Kubernetes apt repository (both nodes)

Install prerequisites, create the keyrings directory (if missing), fetch the GPG key, and add the Kubernetes apt repository:
If apt warns about duplicate package sources (e.g., “Target Packages … is configured multiple times”), that indicates duplicate entries in /etc/apt/sources.list or /etc/apt/sources.list.d/. These warnings can be cleaned up but do not block installation.
On older Debian/Ubuntu releases the directory /etc/apt/keyrings may not exist; creating it prevents curl from failing when writing the keyring (as shown above).

3) Install kubeadm, kubelet, kubectl (pinned version) (both nodes)

Pinning the Kubernetes packages prevents unexpected upgrades that can break cluster compatibility. This lab uses Kubernetes v1.26.0 — adjust K8S_VERSION if your environment requires a different release.
Verify kubelet is installed:
Expected output example:
Suggested quick reference: package vs purpose

4) Before initialization: kubectl on control plane

On the control plane, kubectl cannot contact the API server until kubeadm init completes. Running kubectl get nodes before initialization will produce an error similar to:
This is expected; proceed to initialize the control plane.

5) Initialize the control plane with kubeadm (control plane only)

  1. Determine the control plane IP on the interface you want to advertise. For example:
  1. Initialize the control plane, specifying the advertised API server address and the pod network CIDR. Flannel commonly uses 10.244.0.0/16.
During kubeadm init you will see output that includes the join command and instructions to configure kubectl for your regular user. Example snippet:
Follow the init output to configure kubectl for your regular user:
Check node status (it may show NotReady until the pod network is installed):
Example immediate output:
The NotReady state is normal until the network addon pods are running.

6) Join node01 to the cluster (node01 only)

From the kubeadm init output you recorded the kubeadm join ... command. On node01 (run as root or with sudo) execute that join command. Example:
You should see output confirming the node joined successfully:
Back on the control plane, verify both nodes are visible (the worker may still be NotReady until the network is installed):

7) Install a pod network (Flannel) (control plane)

Deploy Flannel using the upstream manifest. This installs the Flannel DaemonSet and related resources into kube-system:
After applying the manifest, wait a moment and verify pods in all namespaces:
You should see Flannel pods in kube-system along with CoreDNS and kube-proxy. When Flannel becomes ready, node statuses will transition to Ready:
Example final output:

Summary

  • Configured required sysctl settings on both nodes and disabled swap.
  • Added the Kubernetes apt repository and installed kubeadm, kubelet, and kubectl pinned to v1.26.0.
  • Initialized the control plane with kubeadm init, advertising the control plane IP and specifying the pod network CIDR for Flannel.
  • Joined node01 using the kubeadm join command produced by kubeadm init.
  • Deployed Flannel as the pod network and verified nodes reached the Ready state.

Troubleshooting & tips

  • If nodes remain NotReady after installing the pod network, check kubectl get pods -n kube-system to see pod states for Flannel/CoreDNS/kube-proxy.
  • Verify the --apiserver-advertise-address passed to kubeadm init is reachable from worker nodes.
  • If kubeadm join fails due to token expiration, create a new token on the control plane:
  • Certificate or time-related errors can often be resolved by synchronizing clocks across nodes (e.g., enable and start systemd-timesyncd or chronyd).
  • If kubelet uses the wrong cgroup driver, ensure container runtime (containerd) is configured to use systemd cgroup driver and restart the runtime.

If you run into specific errors, include the exact kubeadm output and kubectl -n kube-system get pods output so you can be assisted more quickly.

Watch Video