Set up a Kubernetes cluster on Ubuntu with kubeadm
Build a single-control-plane Kubernetes cluster with containerd, an explicit Pod network, a joined worker, verification, and a complete reset path.
Create a reproducible Kubernetes learning or small self-managed cluster without hiding the host, network, runtime, or recovery requirements.
- Ubuntu Server 24.04 LTS
- Kubernetes 1.36.x
- containerd 1.7+, 2.x
- Recovery access Keep console access to every node before changing swap, kernel networking, or the container runtime.
- Node capacity Provide at least 2 CPUs and 2 GiB RAM for the control plane, plus one or more Linux workers.
nproc && free --gibi - Network plan Choose stable node addresses and a Pod CIDR that does not overlap host, VPN, or site networks.
ip route show - Clean hosts Use nodes that are not already running another Kubernetes distribution or a conflicting CNI.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- A two-node Kubernetes 1.36 learning or small self-managed cluster on Ubuntu 24.04 with one control plane, one worker, containerd, and a single pinned Flannel CNI release.
- Hosts with deliberate swap, kernel forwarding, cgroup, package-version, port, route, and Pod CIDR decisions instead of hidden installer defaults.
- A protected cluster-admin kubeconfig, short-lived bootstrap join material, readiness evidence, a workload and DNS smoke test, and a rehearsable teardown path.
- kubeadm preflight succeeds on every node, containerd and kubelet use compatible systemd cgroups, and packages are held at an intentional supported minor release.
- The API server is reachable only through the planned control-plane endpoint, one CNI owns Pod networking, CoreDNS becomes Running, and every intended node reports Ready.
- A disposable multi-node workload resolves a ClusterIP service and exchanges traffic before the cluster is accepted; credentials, reset limitations, upgrades, backup, and high-availability gaps are documented.
Architecture
How the parts fit together
kubeadm writes static control-plane Pod manifests and a local stacked etcd member on the control-plane node. Kubelet asks containerd through CRI to run Pods, Flannel supplies one CNI network over the non-overlapping 10.244.0.0/16 Pod CIDR, and CoreDNS supplies service discovery. Workers bootstrap through a short-lived token and CA pin, while administrators use a tightly protected kubeconfig. This design is intentionally not highly available.
- Inventory every node and prove unique host identity, stable routing, required ports, DNS/time, capacity, cgroup v2, and non-overlapping networks.
- Disable swap under an explicit policy, load bridge/overlay modules, enable forwarding, and configure containerd consistently.
- Install kubeadm, kubelet, and kubectl from the repository for exactly one Kubernetes minor release, then hold them.
- Initialize the single control plane with the agreed Pod CIDR and CRI socket, protect generated credentials, and install exactly one pinned CNI manifest.
- Wait for control-plane networking and CoreDNS, generate a fresh time-limited join command, and enroll one prepared worker at a time.
- Prove node readiness, system Pods, scheduling, service discovery, and traffic; then record backup, upgrade, capacity, monitoring, and recovery obligations.
Assumptions
- The nodes are dedicated Ubuntu Server 24.04 LTS hosts with unique hostnames, machine IDs, MAC addresses, and stable default-route addresses.
- The control-plane endpoint and every Kubernetes/Flannel port are allowed only between the intended nodes and administrator networks; the API is not casually exposed to the internet.
- Host, VPN, service, and Pod networks do not overlap. The example reserves 10.244.0.0/16 for Pods and must be changed consistently when that range conflicts.
- The hosts use cgroup v2, which Kubernetes 1.36 expects by default, and containerd is the only CRI runtime or its socket is selected explicitly.
- Nodes can reach signed Ubuntu and pkgs.k8s.io repositories plus approved image registries, or an independently designed air-gapped mirror is available.
- A single control plane is accepted as a failure domain for learning or small noncritical use; production availability needs multiple control-plane nodes, a stable endpoint, and tested etcd recovery.
Key concepts
- CRI
- The Kubernetes Container Runtime Interface used by kubelet to manage Pod sandboxes and containers through containerd.
- CNI
- The Container Network Interface implementation that assigns Pod addresses and connects Pod traffic; a cluster uses one primary Pod network.
- Pod CIDR
- An address range reserved for Pods that must not overlap node, service, VPN, or routed site networks.
- cgroup driver
- The resource-control manager used by kubelet and the runtime; on systemd hosts both should use systemd semantics.
- Bootstrap token
- A short-lived secret used with CA public-key pinning to authenticate a node during kubeadm join.
- Static Pod
- A node-local Pod definition watched directly by kubelet; kubeadm places control-plane manifests under /etc/kubernetes/manifests.
- Node Ready
- A condition indicating kubelet reports healthy node status; it does not by itself prove application networking or durable control-plane recovery.
Before you copy
Values used in this guide
{{controlPlaneAddress}}Stable node address advertised by the control-plane API on the cluster network.
Example: 10.20.0.10{{bootstrapToken}}secretShort-lived kubeadm discovery and TLS bootstrap token generated immediately before joining a worker.
Example: abcdef.0123456789abcdef{{caHash}}SHA-256 hash pin of the cluster CA public key used by discovery.
Example: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefSecurity and production boundaries
- `/etc/kubernetes/admin.conf` grants cluster-admin; `/etc/kubernetes/super-admin.conf` bypasses normal authorization through system:masters. Never publish either file.
- Bootstrap tokens are bearer-like enrollment credentials. Generate them only when needed, limit lifetime, transmit through approved channels, and delete unused tokens.
- Restrict TCP/6443 to administrator and node networks. Also account for kubelet TCP/10250, control-plane ports, NodePort policy, and the selected CNI's traffic.
- Do not pipe mutable remote scripts into a shell. Use signed package repositories and a pinned, reviewed CNI release manifest whose provenance is recorded.
- Kubernetes RBAC, NetworkPolicy support, Pod security controls, secrets encryption, audit logging, image policy, backups, and workload hardening remain separate production controls.
- A node is a privileged trust boundary. Root compromise of a control-plane node exposes cluster credentials and potentially etcd data.
Stop before continuing if
- Stop when any hostname, machine ID, MAC, route, clock, runtime endpoint, or Pod CIDR conflicts between nodes.
- Do not initialize until required ports are restricted correctly and the chosen CNI version supports the Kubernetes release and Pod CIDR.
- Do not rerun kubeadm init after a failure without understanding the phase and using documented reset or recovery behavior.
- Do not join workers while CoreDNS or the CNI remains unhealthy; multiplying nodes obscures the original network fault.
- Do not accept the cluster when nodes are merely Ready but scheduling, service DNS, cross-node traffic, backup, monitoring, or upgrade planning is unproven.
- Do not use this single-control-plane topology for a workload whose availability or data durability requires control-plane redundancy.
verification
Inspect every node before changing it
Confirm the OS, CPU, memory, route, unique hostname, and current swap state on the control plane and every worker.
Why this step matters
A complete cross-node inventory prevents bootstrap from embedding duplicate identity, wrong routes, insufficient capacity, clock skew, or overlapping address ranges into cluster state.
What to understand
Record OS, kernel, cgroup version, hostname, machine-id, MAC addresses, CPU, memory, swap, routes, DNS, time synchronization, listeners, and firewall state on every node.
Verify bidirectional node connectivity and the official port matrix: API 6443, kubelet 10250, control-plane 2379-2380/10257/10259, plus CNI-specific traffic.
Compare the proposed 10.244.0.0/16 Pod range with every host, service, VPN, cloud, and site route. An overlap can produce intermittent failures that look like CNI bugs.
System changes
- No intended change; reads hardware, identity, network, swap, cgroup, time, and service state on every node.
Syntax explained
hostnamectl- Shows the node hostname, operating system, kernel, and architecture.
nproc / free --gibi- Reports allocatable CPU count and memory in practical units.
ip route show- Displays route selection used for node address auto-detection.
swapon --show- Enumerates currently active swap devices.
hostnamectl && nproc && free --gibi && ip route show && swapon --showOperating System: Ubuntu 24.04 LTS 4 Mem: 7.7Gi Default via 10.20.0.1 dev ens18 NAME TYPE SIZE USED PRIO
Checkpoint: Approve every node
hostnamectl && systemd-detect-virt && stat -fc %T /sys/fs/cgroup && ip route && swapon --showContinue whenEach node is unique, uses cgroup2fs, has stable intended routes, adequate capacity, synchronized time, and no conflicting CIDR.
Stop whenIdentity duplicates, cgroup v1, route ambiguity, clock skew, insufficient capacity, active unplanned services, or address overlap exists.
If this step fails
Two cloned nodes have the same machine-id.
Likely causeA VM image was cloned without regenerating host identity.
cat /etc/machine-idip -brief link
ResolutionRegenerate identity using the Ubuntu image process before Kubernetes installation; never join duplicate nodes.
Security notes
- Keep control-plane and kubelet ports off public networks; document every permitted source and direction.
Alternatives
- Generate this inventory through reviewed configuration management when it captures the same per-node evidence.
Stop conditions
- Do not alter any node until the complete topology and recovery access are approved.
command
Disable swap and preserve the prior fstab
The default kubeadm preflight rejects active swap. Save the original file before disabling current and boot-time swap.
Why this step matters
The selected kubeadm 1.36 baseline rejects active swap, so preserving and explicitly disabling every swap source makes kubelet resource behavior predictable and rollback possible.
What to understand
The command archives fstab before editing and disables current swap. Inspect zram, cloud-init, and systemd-generated swap separately.
The sed expression comments fstab swap rows with an audit marker; review the diff before reboot rather than assuming every row matched correctly.
Reboot a non-control-plane host during validation and confirm swap remains absent before repeating the change across nodes.
System changes
- Archives `/etc/fstab`, disables active swap, and comments boot-time swap entries.
Syntax explained
cp --archive- Preserves the original fstab metadata for exact rollback.
swapoff --all- Disables all currently active swap devices.
sed ... swap- Comments fstab rows whose filesystem type is swap and labels the change.
sudo cp --archive /etc/fstab /etc/fstab.before-kubernetes && sudo swapoff --all && sudo sed -i '/[[:space:]]swap[[:space:]]/ s/^/# oneliners: /' /etc/fstabswapon --show returns no rows
Checkpoint: Prove swap remains disabled
swapon --show && grep -nE '^[^#].*[[:space:]]swap[[:space:]]' /etc/fstab || trueContinue whenNeither command reports an active or uncommented boot-time swap entry.
Stop whenSwap remains active, memory headroom is unsafe, fstab diff affects non-swap mounts, or another generator will recreate swap.
If this step fails
Swap returns after reboot.
Likely causezram, cloud-init, a systemd unit, or an unmodified fstab entry recreates it.
swapon --showsystemctl list-units --type=swapsystemctl list-unit-files | grep -i zram
ResolutionDisable the actual provider under the approved policy and repeat a reboot check before kubeadm.
Security notes
- Disabling swap can expose memory pressure; capacity and eviction design must replace it, not denial of the risk.
Alternatives
- Use Kubernetes swap support only through a separately validated kubelet policy supported by the chosen release.
Stop conditions
- Stop if node capacity cannot safely operate without swap or the prior fstab cannot be restored.
config
Load bridge networking and forwarding settings
Persist the kernel modules and sysctls required for bridged Pod traffic, then verify the applied values.
Why this step matters
Overlay networking and bridged packet visibility depend on persistent kernel modules and forwarding values that must survive reboot consistently across every node.
What to understand
overlay supports common container snapshotters; br_netfilter exposes bridged IPv4/IPv6 traffic to netfilter hooks used by cluster networking.
IPv4 forwarding permits routed Pod traffic. The bridge sysctls must exist only after br_netfilter is loaded.
Review existing sysctl ownership to avoid two automation systems fighting over the same keys.
System changes
- Creates module-load and sysctl drop-ins, loads two modules, enables bridge netfilter hooks, and enables IPv4 forwarding.
Syntax explained
/etc/modules-load.d/kubernetes.conf- Requests overlay and br_netfilter on future boots.
modprobe- Loads the modules immediately for current validation.
sysctl --system- Reloads system sysctl configuration in precedence order.
net.ipv4.ip_forward=1- Allows the host to route IPv4 traffic between interfaces.
/etc/sysctl.d/99-kubernetes-cri.confprintf 'overlay\nbr_netfilter\n' | sudo tee /etc/modules-load.d/kubernetes.conf && sudo modprobe overlay && sudo modprobe br_netfilter && printf 'net.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\n' | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf && sudo sysctl --systemnet.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1
Checkpoint: Verify networking prerequisites
lsmod | grep -E '^(overlay|br_netfilter)' && sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forwardContinue whenBoth modules are loaded and all three values equal 1 on every node.
Stop whenA value is overridden, a module is unavailable, or host firewall policy does not account for forwarded Pod traffic.
If this step fails
The bridge sysctl path does not exist.
Likely causebr_netfilter failed to load or the running kernel lacks the module.
sudo modprobe br_netfilterdmesg | tail -n 50
ResolutionInstall or boot a supported Ubuntu kernel, load the module, then apply and verify the sysctls.
Security notes
- Forwarding expands the host data path; enforce node and CNI policy instead of assuming default-deny input covers forwarding.
Alternatives
- Use the exact prerequisites documented by another selected CNI when it does not use bridged overlay behavior.
Stop conditions
- Do not bootstrap with inconsistent module or forwarding state across nodes.
command
Install and configure containerd
Generate a known runtime configuration, select the systemd cgroup driver, and verify the service before installing Kubernetes.
Why this step matters
Kubelet and containerd must agree on CRI availability and the systemd cgroup hierarchy before cluster certificates and node state are created.
What to understand
Generate a complete packaged default configuration instead of maintaining an undocumented fragment, then change only the cgroup setting.
Kubernetes 1.36 expects cgroup v2 by default; containerd and kubelet should use systemd semantics on Ubuntu.
Verify the service, socket, effective config, and CRI response. A running process alone does not prove a usable CRI plugin.
System changes
- Installs containerd, writes `/etc/containerd/config.toml`, selects SystemdCgroup, and restarts the runtime.
Syntax explained
containerd config default- Emits a complete default configuration for the installed runtime version.
SystemdCgroup = true- Delegates container cgroup lifecycle through systemd.
systemctl restart containerd- Applies the generated runtime configuration.
systemctl is-active- Returns success only when systemd considers the runtime active.
sudo apt update && sudo apt install --yes containerd && sudo install -d -m 0755 /etc/containerd && containerd config default | sudo tee /etc/containerd/config.toml >/dev/null && sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml && sudo systemctl restart containerd && systemctl is-active containerdactive
Checkpoint: Prove CRI and cgroups
systemctl is-active containerd && sudo ctr version && sudo grep -n 'SystemdCgroup = true' /etc/containerd/config.tomlContinue whencontainerd is active, client/server versions respond, and the effective runtime stanza uses SystemdCgroup.
Stop whenCRI is disabled, the socket is missing, runtime logs show errors, or cgroup mode differs.
If this step fails
containerd starts but kubeadm cannot find a CRI endpoint.
Likely causeThe CRI plugin is disabled, configuration schema differs, or multiple runtime sockets confuse detection.
sudo ctr plugins ls | grep crisudo journalctl -u containerd --since '10 minutes ago' --no-pagersudo ls -l /run/containerd/containerd.sock
ResolutionRestore a valid version-appropriate config with CRI enabled and pass the explicit containerd socket to kubeadm.
Security notes
- Restrict write access to runtime configuration and socket; membership or root access to the runtime is host-equivalent privilege.
Alternatives
- Use another Kubernetes-supported CRI runtime with its official systemd-cgroup configuration.
Stop conditions
- Do not install cluster state until CRI health and cgroup agreement are proven.
command
Install kubelet, kubeadm, and kubectl
Use the versioned Kubernetes package repository, then hold the node packages so ordinary upgrades cannot introduce unsupported version skew.
Why this step matters
A repository scoped to one Kubernetes minor and explicit package holds prevent ordinary host upgrades from creating unsupported control-plane, kubelet, and kubeadm version skew.
What to understand
pkgs.k8s.io repositories are minor-version-specific. A future minor upgrade requires repository and lifecycle changes, not a casual package update.
Install the repository signing key into a dedicated keyring and reference it with signed-by; never use apt-key or disable verification.
Inspect package candidates and installed client versions, then hold all three packages until a documented kubeadm upgrade procedure unholds them.
System changes
- Adds the Kubernetes 1.36 APT keyring and repository, installs kubelet/kubeadm/kubectl, and marks them held.
Syntax explained
curl -fsSL- Fails on HTTP errors while retrieving the repository signing key over TLS.
gpg --dearmor- Converts the armored public key into an APT keyring file.
signed-by=- Scopes repository trust to the dedicated Kubernetes keyring.
apt-mark hold- Prevents unattended package movement outside the cluster upgrade sequence.
sudo install -m 0755 -d /etc/apt/keyrings && curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/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.36/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list && sudo apt update && sudo apt install --yes kubelet kubeadm kubectl && sudo apt-mark hold kubelet kubeadm kubectlkubelet set on hold. kubeadm set on hold. kubectl set on hold.
Checkpoint: Pin the intended release
kubeadm version -o short && kubectl version --client && apt-mark showhold | grep -E '^(kubeadm|kubelet|kubectl)$'Continue whenTools report v1.36.x and all three package names appear in the hold list.
Stop whenRepository signature fails, candidates span the wrong minor, versions differ unexpectedly, or packages are not held.
If this step fails
APT has no kubeadm candidate.
Likely causeThe repository URL, architecture, keyring, DNS, or minor path is wrong.
apt-cache policy kubeadmsudo apt update
ResolutionCorrect the official v1.36 repository definition and keyring; do not fall back to the frozen legacy repository.
Security notes
- Package holds require an explicit patch cadence; forgotten holds leave security fixes unapplied.
Alternatives
- Mirror the same signed packages internally when supply-chain and availability policy requires it.
Stop conditions
- Do not mix Kubernetes minor releases or continue after repository verification errors.
warning
Initialize the control plane
Run this only on the intended control-plane node. The Pod CIDR must match the selected CNI and must not overlap any existing network.
Why this step matters
Control-plane initialization creates PKI, stacked etcd, static Pods, kubelet configuration, and cluster state, so network and recovery decisions must be final first.
What to understand
Run only on the intended control-plane node. The 10.244.0.0/16 value is coupled to the Flannel manifest and must not overlap another route.
kubeadm performs preflight and then writes sensitive files under `/etc/kubernetes` plus etcd state under `/var/lib/etcd`.
Capture output securely because it includes a bootstrap join command; protect super-admin.conf separately as break-glass material.
System changes
- Creates cluster PKI, kubeconfigs, static control-plane manifests, stacked etcd state, kubelet bootstrap state, and a bootstrap token.
Syntax explained
kubeadm init- Runs preflight and initializes a new control plane on this node.
--pod-network-cidr=10.244.0.0/16- Declares the Pod address pool expected by the selected Flannel configuration.
sudo kubeadm init --pod-network-cidr=10.244.0.0/16Your Kubernetes control-plane has initialized successfully! You should now deploy a Pod network to the cluster.
Checkpoint: Accept control-plane initialization
sudo KUBECONFIG=/etc/kubernetes/admin.conf kubectl get --raw=/readyz?verboseContinue whenkubeadm reports success and the local authenticated API readiness checks pass.
Stop whenAny preflight is ignored without justification, init fails mid-phase, API readiness fails, or the Pod CIDR is wrong.
If this step fails
kubeadm init times out waiting for the control plane.
Likely causeKubelet, containerd, cgroups, image pulls, node address selection, or required ports prevent static Pods from becoming healthy.
sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps -asudo journalctl -u kubelet --since '15 minutes ago' --no-pagersudo ss -lntp | grep 6443
ResolutionDiagnose the first failing component; preserve evidence and use documented reset only when abandoning the partial cluster.
Security notes
- Never post init output, admin.conf, super-admin.conf, certificates, or bootstrap tokens in public logs.
Alternatives
- Use a reviewed kubeadm configuration file when stable endpoints, certificate SANs, dual stack, or reproducible advanced settings are required.
Stop conditions
- Do not rerun init over partial state or bypass preflight errors to force success.
command
Configure kubectl for the administrator
Copy the generated admin kubeconfig with restrictive ownership. Treat this file as a cluster-admin credential and never publish it.
Why this step matters
Copying admin.conf with owner-only permissions enables administration without running kubectl as root while recognizing that the credential grants complete cluster control.
What to understand
The copied certificate belongs to kubeadm:cluster-admins and is bound to cluster-admin; use it for commissioning, not broad distribution.
Keep super-admin.conf offline as tightly controlled break-glass material because system:masters bypasses normal authorization.
Verify context, API endpoint, file mode, and authenticated identity before applying the CNI.
System changes
- Creates `$HOME/.kube/config` containing a cluster-admin client certificate and endpoint configuration.
Syntax explained
cp -i- Prompts before replacing an existing kubeconfig.
chown $(id -u):$(id -g)- Assigns the current administrator ownership.
chmod 600- Restricts kubeconfig reading and modification to its owner.
mkdir -p "$HOME/.kube" && sudo cp -i /etc/kubernetes/admin.conf "$HOME/.kube/config" && sudo chown "$(id -u):$(id -g)" "$HOME/.kube/config" && chmod 600 "$HOME/.kube/config"kubectl config current-context kubernetes-admin@kubernetes
Checkpoint: Protect cluster-admin access
stat -c '%a %U:%G %n' "$HOME/.kube/config" && kubectl auth whoami && kubectl config view --minifyContinue whenMode is 600, the expected administrator identity appears, and the endpoint matches the intended API.
Stop whenAn existing context was overwritten unexpectedly, permissions are broad, identity is wrong, or endpoint is public/unplanned.
If this step fails
kubectl reports connection refused.
Likely causeThe context endpoint is wrong, API static Pod is unhealthy, firewall blocks it, or KUBECONFIG selects another file.
kubectl config view --minifysudo ss -lntp | grep 6443printf '%s\n' "$KUBECONFIG"
ResolutionSelect the intended config and repair API health or network policy; do not disable TLS verification.
Security notes
- Never store admin.conf in source control, shared home directories, images, or unrestricted backup artifacts.
Alternatives
- Generate individual kubeconfigs and bind least-privilege RBAC after commissioning.
Stop conditions
- Do not apply cluster resources through an unverified context or identity.
command
Install one Pod network
Apply the pinned Flannel v0.28.4 manifest for the chosen 10.244.0.0/16 Pod CIDR. Do not install a second CNI on the same cluster.
Why this step matters
Installing one pinned CNI release completes Pod networking and allows CoreDNS to run without trusting a mutable latest URL or layering competing datapaths.
What to understand
The manifest is pinned to Flannel v0.28.4. Download, inspect, and archive the exact manifest in controlled environments before applying it.
The manifest expects 10.244.0.0/16 unless reviewed and changed consistently with kubeadm configuration.
Observe the kube-flannel namespace, DaemonSet rollout, node routes, CoreDNS, and image pulls before joining workers.
System changes
- Creates the kube-flannel namespace, RBAC, configuration, service account, and node DaemonSet for the pinned CNI release.
Syntax explained
kubectl apply -f- Reconciles the objects declared in the selected manifest.
v0.28.4 URL- Pins review and deployment to a specific upstream Flannel release.
10.244.0.0/16- Must match both kubeadm cluster configuration and the CNI network configuration.
kubectl apply -f https://github.com/flannel-io/flannel/releases/download/v0.28.4/kube-flannel.ymlnamespace/kube-flannel created daemonset.apps/kube-flannel-ds created
Checkpoint: Prove one healthy CNI
kubectl rollout status daemonset/kube-flannel-ds -n kube-flannel --timeout=180s && kubectl wait -n kube-system --for=condition=Ready pod -l k8s-app=kube-dns --timeout=180sContinue whenFlannel is available on the control-plane node and every CoreDNS Pod becomes Ready.
Stop whenThe manifest source/version is unverified, another CNI exists, DaemonSet fails, or CoreDNS remains unhealthy.
If this step fails
Flannel Pod enters CrashLoopBackOff.
Likely causePod CIDR mismatch, missing kernel prerequisites, inaccessible image, interface selection, firewall, or stale prior CNI state.
kubectl logs -n kube-flannel daemonset/kube-flannel-ds --all-containers --tail=100kubectl describe pod -n kube-flannel -l app=flannelip route show
ResolutionCorrect the single root cause and restart the rollout; do not install another CNI beside it.
Security notes
- Archive the reviewed manifest and monitor CNI security releases; Flannel alone does not enforce NetworkPolicy.
Alternatives
- Select a supported NetworkPolicy-capable CNI before initialization when policy enforcement is required.
Stop conditions
- Do not join workers until CNI and CoreDNS are healthy on the control plane.
command
Join each worker with a fresh token
Run the generated join command on one prepared worker at a time. The token is a credential; generate a new command if it has expired and do not store it in shell history longer than necessary.
Why this step matters
A fresh short-lived token and pinned CA hash enroll exactly one prepared worker without disabling discovery verification or reusing stale bootstrap credentials.
What to understand
Generate `kubeadm token create --print-join-command --ttl` immediately before enrollment and transmit it through an approved channel.
Run the command on one worker at a time, confirm its unique identity and expected API address, then observe CSR and node status.
Delete unused tokens after enrollment; the CA hash is not secret, but the bootstrap token is.
System changes
- Creates worker kubelet PKI, kubeconfig, CNI/runtime state, Node registration, and bootstrap authentication records.
Syntax explained
kubeadm join host:6443- Bootstraps this host against the selected API endpoint.
--token- Supplies the time-limited discovery and TLS bootstrap secret.
--discovery-token-ca-cert-hash sha256:- Pins the cluster CA public key and prevents blind trust of an endpoint.
Values stay on this page and are never sent or saved.
sudo kubeadm join {{controlPlaneAddress}}:6443 --token {{bootstrapToken}} --discovery-token-ca-cert-hash sha256:{{caHash}}This node has joined the cluster: * Certificate signing request was sent to apiserver and a response was received.
Checkpoint: Accept one worker
kubectl get nodes -o wide && sudo kubeadm token listContinue whenThe uniquely named worker appears and progresses to Ready; only expected unexpired tokens remain.
Stop whenNode identity is duplicated, the endpoint/hash is unexpected, join partially fails, or a token is exposed.
If this step fails
The token is invalid or expired.
Likely causeIts TTL elapsed, it was deleted, or the token text was copied incorrectly.
sudo kubeadm token listkubectl get csr
ResolutionGenerate a new short-lived join command on the control plane and delete abandoned tokens; never use unsafe discovery flags.
Security notes
- Treat token values as secrets and avoid persistent shell history, tickets, screenshots, and chat transcripts.
Alternatives
- Use a kubeadm JoinConfiguration file delivered through an approved secret channel for reproducible advanced enrollment.
Stop conditions
- Do not add another worker until the current join either succeeds cleanly or is reset and explained.
verification
Wait for nodes and system Pods
Do not deploy workloads until every intended node is Ready and CoreDNS plus the CNI are running.
Why this step matters
Node Ready and Running system Pods are necessary but insufficient evidence, so final acceptance must include scheduling, DNS, service endpoints, and cross-node traffic.
What to understand
Inspect node roles, versions, internal addresses, conditions, taints, system Pods, restart counts, and events before deploying a test workload.
Create a disposable multi-replica web workload spread across nodes, expose it internally, and resolve/curl it from a temporary client Pod.
Delete the smoke namespace afterward and confirm all remaining system Pods are healthy.
System changes
- Reads cluster health; a full acceptance drill may temporarily create and then delete an isolated smoke-test namespace and workloads.
Syntax explained
get nodes -o wide- Shows readiness, role, version, internal address, OS, kernel, and runtime.
get pods --all-namespaces- Inventories system and workload Pod readiness plus restart counts.
--field-selector- Can filter out Running/Succeeded states to focus on unhealthy Pods.
kubectl get nodes -o wide && kubectl get pods --all-namespacesNAME STATUS ROLES VERSION control-1 Ready control-plane v1.36.x worker-1 Ready <none> v1.36.x kube-system coredns-... 1/1 Running
Checkpoint: Accept the cluster data path
kubectl wait --for=condition=Ready nodes --all --timeout=180s && kubectl get pods -A -o wideContinue whenEvery intended node is Ready, system Pods are healthy, and a documented smoke test proves DNS and service traffic.
Stop whenAny node, system Pod, DNS lookup, endpoint, cross-node request, or cleanup step fails.
If this step fails
Nodes are Ready but test Pods remain Pending.
Likely causeControl-plane taints, insufficient worker resources, image pulls, selectors, PVCs, or admission policy prevent scheduling.
kubectl describe pod -n smokekubectl get events -n smoke --sort-by=.lastTimestamp
ResolutionCorrect the explicit scheduler or admission reason; do not remove control-plane isolation merely to hide missing worker capacity.
Security notes
- Use non-sensitive images and data in smoke tests; avoid granting them host access or unnecessary service-account privileges.
Alternatives
- Run an organization-owned conformance or synthetic test suite that proves the same control and data paths.
Stop conditions
- Do not call the cluster ready based only on kubeadm success text.
Finish line
Verification checklist
kubectl wait --for=condition=Ready nodes --all --timeout=180sEvery registered node reports condition met.kubectl get pods --all-namespaces --field-selector=status.phase!=Running,status.phase!=SucceededReturns no unhealthy Pods.kubectl cluster-infoReports reachable control-plane and CoreDNS endpoints.Recovery guidance
Common problems and safe checks
kubeadm preflight rejects active swap.
Likely causeA swap partition, file, zram device, or systemd generator remains active or returns after reboot.
swapon --showfindmnt --types swapgrep -nE '[[:space:]]swap[[:space:]]' /etc/fstab
ResolutionIdentify every swap provider, preserve its prior configuration, disable it according to the approved node policy, reboot if needed, and rerun preflight.
kubelet repeatedly restarts before or after initialization.
Likely causeBefore init this may be expected; afterward common causes include swap, cgroup mismatch, an unreachable runtime socket, missing configuration, or certificate failure.
systemctl status kubelet --no-pagerjournalctl -u kubelet --since '15 minutes ago' --no-pagersudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock info
ResolutionUse the first concrete kubelet/runtime error, correct that host prerequisite, and avoid deleting PKI or state speculatively.
CoreDNS stays Pending after kubeadm init.
Likely causeNo CNI is installed yet, the CNI DaemonSet is unhealthy, the Pod CIDR conflicts, required kernel features are missing, or image pulls fail.
kubectl get pods -A -o widekubectl get daemonset -n kube-flannelkubectl describe pod -n kube-system -l k8s-app=kube-dns
ResolutionRepair the single selected CNI and host networking first; do not install a second CNI as a workaround.
A worker cannot join the API server.
Likely causeTCP/6443 routing or filtering fails, the token expired, the CA hash is wrong, time is skewed, or node identity duplicates another host.
nc -vz 10.20.0.10 6443sudo kubeadm token listtimedatectl status
ResolutionRestore the restricted network path, generate a fresh join command on the control plane, verify the CA pin, and reset only the failed worker if partial state blocks retry.
Nodes are Ready but a test service cannot resolve or communicate across nodes.
Likely causeCoreDNS, kube-proxy, CNI routes, host firewall, MTU, reverse-path filtering, or overlapping networks break the data path.
kubectl get pods -A -o widekubectl get svc,endpointslices -Aip route show && sudo nft list ruleset
ResolutionTrace DNS, service endpoint, and Pod-to-Pod layers separately; correct the owning layer instead of disabling all firewalling.
Reference
Frequently asked questions
Is kubeadm a production installer?
It is a supported bootstrap building block, not a complete production platform. Availability, infrastructure, storage, ingress, policy, monitoring, upgrades, and recovery remain operator responsibilities.
Why is CoreDNS Pending before Flannel?
Cluster DNS Pods require a functional Pod network. Pending before CNI can be expected; remaining Pending afterward signals a scheduling or network problem.
Can I install two CNI plugins?
Not as two competing primary Pod networks. Use exactly one supported design unless a documented multi-network architecture deliberately introduces secondary attachments.
Does kubeadm reset restore the original host?
No. Reset is best effort and does not automatically remove every CNI interface, iptables/IPVS rule, kubeconfig, package, sysctl, module, or application volume.
Recovery
Rollback
Reset Kubernetes state on workers first, then the control plane, and restore the host settings only after the cluster is no longer needed.
- Drain and delete workload nodes from the control plane when the cluster contains data that must be preserved.
- Run sudo kubeadm reset on each worker and then on the control-plane node.
- Remove the CNI interfaces and Kubernetes directories identified by kubeadm reset; do not delete unrelated network configuration.
- Restore /etc/fstab.before-kubernetes, remove the Kubernetes sysctl and module files, and reboot or re-enable the previous swap policy.
- Remove package holds only when intentionally uninstalling or changing Kubernetes versions.
Evidence