Kubernetes Node Setup
Overview
A Kubernetes node running in production requires careful configuration at the OS level — kernel parameters for networking, container runtime setup, security hardening, and proper joining procedures. This reference covers worker and control plane node provisioning from bare metal to production-ready.
Kernel parameters
Add to /etc/sysctl.d/99-kubernetes.conf and apply with sysctl --system:
# Bridge traffic to iptables (required for pod networking)
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
# IP forwarding (required for pod-to-pod communication)
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
# Increase connection tracking table
net.netfilter.nf_conntrack_max = 1048576
# Inotify watches (for kubelet and container runtime)
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 8192
# Increase file descriptor limits
fs.file-max = 2097152
# Core dump pattern (for debugging)
kernel.core_pattern = /var/log/core.%e.%p.%t
Container runtime (containerd)
Installation (Debian/Ubuntu)
On Debian/Ubuntu, install containerd from the distribution packages. Afterwards, generate the default configuration and flip on the SystemdCgroup driver — the kubelet requires it to manage container cgroups correctly.
# Install containerd
apt-get update
apt-get install -y containerd
# Generate default config
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
# Enable the SystemdCgroup driver (CRITICAL: the kubelet requires
# this cgroup driver to manage container resources correctly)
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# Restart
systemctl restart containerd
containerd config.toml (essential sections)
The generated config.toml has a few sections that matter for Kubernetes. The essential ones are the cgroup driver, the pause/sandbox image, and registry mirror endpoints for pulling container images.
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
[plugins."io.containerd.grpc.v1.cri"]
sandbox_image = "registry.k8s.io/pause:3.9"
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://registry-1.docker.io"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."<custom-registry>"]
endpoint = ["https://<custom-registry>"]
Worker node setup
Prerequisites
Before installing the Kubernetes binaries, prepare the node at the OS level: disable swap (the kubelet refuses to run with it enabled), load the kernel modules needed for overlay networking, apply the sysctl settings, and add the Kubernetes apt repository.
# Disable swap (REQUIRED by kubelet)
swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab
# Load kernel modules
cat > /etc/modules-load.d/kubernetes.conf <<EOF
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter
# Apply sysctl
sysctl --system
# Install kubeadm, kubelet, kubectl (Debian)
apt-get install -y apt-transport-https ca-certificates curl
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | \
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/ /" | \
tee /etc/apt/sources.list.d/kubernetes.list
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
kubelet configuration
The kubelet config file controls how the node runs pods: the maximum number of pods, how much CPU/memory is reserved for the system and kubelet itself, and when to evict pods under memory or disk pressure.
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
maxPods: 110
systemReserved:
cpu: "500m"
memory: "512Mi"
kubeReserved:
cpu: "500m"
memory: "512Mi"
evictionHard:
memory.available: "500Mi"
nodefs.available: "10%"
imagefs.available: "15%"
evictionSoft:
memory.available: "1Gi"
nodefs.available: "15%"
evictionSoftGracePeriod:
memory.available: "1m30s"
featureGates:
GracefulNodeShutdown: true
Joining the cluster
On the control plane, generate a join token and command, then run it on each worker node. The command carries the API server address, a short-lived token, and the CA hash that proves the worker is joining the right cluster.
# On the control plane node, generate a join command
kubeadm token create --print-join-command
# Run on each worker node
kubeadm join <control-plane-ip>:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
Control plane node setup
kubeadm init
kubeadm init bootstraps the entire control plane. The pod and service CIDRs must match what your CNI plugin expects, and --control-plane-endpoint is the address nodes and clients use to reach the API server.
kubeadm init \
--pod-network-cidr=10.244.0.0/16 \
--service-cidr=10.96.0.0/12 \
--control-plane-endpoint=k8s-api.internal:6443 \
--upload-certs
Networking (CNI)
The CNI plugin provides the cluster's pod network — without one, nodes stay NotReady. Choose a plugin to fit your needs: Flannel for simplicity, Calico for network policies, or Cilium for eBPF-based performance and security features.
# Flannel (simple)
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kubeconfig.yml
# Calico (feature-rich, supports network policies)
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28/manifests/calico.yaml
# Cilium (eBPF-based, high performance)
helm install cilium cilium/cilium \
--namespace kube-system \
--set ipam.mode=kubernetes
Control plane component configuration
Control plane components can be configured via kubeadm ClusterConfiguration:
# kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
networking:
podSubnet: "10.244.0.0/16"
serviceSubnet: "10.96.0.0/12"
apiServer:
extraArgs:
enable-admission-plugins: "NodeRestriction,PodSecurity"
audit-log-path: "/var/log/kube-apiserver-audit.log"
audit-policy-file: "/etc/kubernetes/audit-policy.yaml"
controllerManager:
extraArgs:
bind-address: "0.0.0.0"
scheduler:
extraArgs:
bind-address: "0.0.0.0"
Node maintenance
Cordoning and draining
Before maintenance on a node, cordon it to stop new pods from scheduling, then drain it to evict the running pods gracefully. When the work is done, uncordon makes the node schedulable again.
# Mark node as unschedulable (no new pods)
kubectl cordon worker-1
# Drain all pods (evict gracefully + cordon)
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data
# Uncordon (make schedulable again)
kubectl uncordon worker-1
Node updates
Upgrade kubelet and kubeadm on each node in order, restarting the kubelet after the packages land. Control plane nodes additionally need kubeadm upgrade plan and apply to move the control plane components to the new version.
# Update packages on a node
apt-get update && apt-get upgrade -y kubelet kubeadm kubectl
systemctl restart kubelet
# For control plane nodes, also upgrade components
kubeadm upgrade plan
kubeadm upgrade apply v1.30.1
Node deletion
To remove a node permanently, delete it from the cluster first, then reset kubeadm on the node and clean its state directories. This leaves the machine ready to be re-provisioned cleanly later.
kubectl delete node worker-1
# Then on the node itself:
kubeadm reset -f
rm -rf /etc/cni /etc/kubernetes /var/lib/kubelet /var/lib/etcd
Storage
Provisioning storage on nodes
Nodes can provide persistent storage through hostPath volumes or by mounting an external disk. Prepare the directory or filesystem and register the mount in /etc/fstab so it survives reboots.
# Create a directory for hostPath volumes
mkdir -p /data/volumes
chmod 777 /data/volumes
# Mount an external disk for persistent storage
mkfs.ext4 /dev/sdb
mkdir -p /mnt/kubernetes
echo "/dev/sdb /mnt/kubernetes ext4 defaults 0 2" >> /etc/fstab
mount -a
Security hardening
These CIS-inspired checks lock down a node: restrict permissions on config files, require TLS, and disable anonymous API access. Run through them before putting a node into production.
# CIS benchmark essentials
# Protect kernel tunables
chmod 600 /etc/sysctl.d/*
# Restrict kubelet config
chmod 600 /var/lib/kubelet/config.yaml
# Ensure kubelet uses TLS
kubectl get nodes -o jsonpath='{.items[*].status.conditions[?(@.reason=="KubeletHasSufficientMemory")]}'
# Restrict API server access
# Ensure --anonymous-auth=false on API server
# Ensure --authorization-mode includes RBAC
See also
- Kubernetes Configuration Reference — kubelet config, static pods
- Amazon EKS — managed node groups on AWS