Running Kubernetes on bare‑metal gives you the raw performance of your hardware without the overhead of a cloud hypervisor. In this guide we’ll walk through every phase—from prepping the servers to a fully functional, production‑ready cluster—so you can harness the power of Kubernetes on your own racks.
What You’ll Need
- At least two physical servers (one control‑plane, one or more workers)
- Ubuntu Server 22.04 LTS (or another supported distro) installed on each node
- Static IP addresses for every node
- Root or sudo access on all machines
- Network switch with VLAN support (optional but recommended)
- Basic Linux networking knowledge
Step 1: Prepare the Hardware and BIOS
Before the OS even boots, make sure the BIOS settings are optimized for Kubernetes. Disable Secure Boot, enable VT‑x/AMD‑V for virtualization, and turn on hardware‑assisted I/O (SR‑IOV) if you plan to expose NICs directly to pods. Set the boot order to prioritize your installation media, and enable PXE boot if you’ll be provisioning nodes via network. After saving changes, power on each server and verify that the system clock is synchronized via NTP; time drift can break TLS handshakes between control‑plane components.
Step 2: Install a Minimal OS
Boot from the Ubuntu Server installer and choose the “Minimal installation” option. During partitioning, allocate a dedicated /var/lib/kubelet partition (e.g., 50 GB) to keep container data isolated from the root filesystem. After the install, run:
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y curl gnupg2 software-properties-common
Disable swap immediately—Kubernetes will refuse to start with swap enabled:
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
Enable the firewall (ufw) but open only the ports Kubernetes needs (see the official docs for a full list).
Step 3: Configure Networking and Hostnames
Assign each node a static IP and a meaningful hostname (e.g., k8s-master, k8s-worker01). Edit /etc/hosts on every machine so they can resolve each other without DNS:
192.168.10.10 k8s-master
192.168.10.11 k8s-worker01
192.168.10.12 k8s-worker02
Set the net.ipv4.ip_forward kernel parameter to enable pod networking:
sudo sysctl -w net.ipv4.ip_forward=1
sudo bash -c "echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.d/k8s.conf"
Persist the change with sudo sysctl --system. Verify that the firewall allows traffic on ports 6443, 10250, 10251, and 10252 on the control plane, and ports 30000‑32767 for NodePort services on workers.
Step 4: Install Container Runtime (containerd)
Kubernetes no longer ships with Docker by default; containerd is the recommended runtime. Install it with:
sudo apt-get install -y containerd
sudo systemctl enable containerd
sudo systemctl start containerd
Generate the default configuration and tweak the cgroup driver to match kubelet (systemd):
sudo mkdir -p /etc/containerd
sudo 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
Confirm the runtime works:
ctr version
Step 5: Install kubeadm, kubelet, and kubectl
Add the official Kubernetes apt repository and install the three binaries:
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo add-apt-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"
sudo apt-get update
sudo apt-get install -y kubelet=1.28.0-00 kubeadm=1.28.0-00 kubectl=1.28.0-00
sudo apt-mark hold kubelet kubeadm kubectl
Enable the kubelet service:
sudo systemctl enable kubelet
Step 6: Initialize the Control Plane
On the master node, decide on a pod network CIDR (we’ll use Calico’s 192.168.0.0/16). Then run:
sudo kubeadm init
--control-plane-endpoint "k8s-master:6443"
--upload-certs
--pod-network-cidr=192.168.0.0/16
--service-cidr=10.96.0.0/12
The command outputs a kubeadm join line; copy it for later. To start using the cluster, configure your user’s kubeconfig:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Verify the control plane is healthy:
kubectl get nodes
kubectl get pods -n kube-system
Step 7: Join Worker Nodes
On each worker, run the exact kubeadm join command printed earlier. For example:
sudo kubeadm join k8s-master:6443
--token abcdef.0123456789abcdef
--discovery-token-ca-cert-hash sha256:1234567890abcdef...
--control-plane
After a minute, the master should list the new nodes:
kubectl get nodes
If a node shows NotReady, check journalctl -u kubelet for errors such as mismatched cgroup drivers or missing container runtime.
Step 8: Deploy a Pod Network (Calico)
Kubernetes won’t schedule pods until a CNI plugin is installed. Apply Calico’s manifest:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/calico.yaml
Wait for all Calico pods to reach Running:
kubectl get pods -n kube-system -w
When the network is up, you can launch a test deployment:
kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80 --type=NodePort
Visit any worker’s IP at the allocated NodePort (e.g., http://192.168.10.11:30007) to confirm traffic flows through the cluster.
Common Mistakes to Avoid
1 Leaving swap on. Even a small swap file will cause kubelet to abort. Double‑check with free -h.
2 Mismatched cgroup drivers. kubelet defaults to systemd; if containerd uses cgroupfs, pods will fail to start. Align both by editing /etc/containerd/config.toml and /var/lib/kubelet/config.yaml.
3 Incorrect firewall rules. Blocking inter‑node ports leads to “connection refused” errors in kubectl get cs.
4 Using the wrong pod CIDR. The CIDR in kubeadm init must match the CNI’s configuration; otherwise pods cannot obtain IPs.
5 Skipping time sync. NTP drift breaks certificate validation, causing the API server to reject requests.
Tips and Tricks
• Use a dedicated management VLAN. Isolating control‑plane traffic reduces latency and improves security.
• Enable audit logging. Add --audit-log-path=/var/log/kube-apiserver-audit.log to the API server manifest for compliance.
• Automate node provisioning. Tools like Metal³ or Raspberry Pi Cluster scripts can spin up identical nodes via PXE and cloud‑init.
• Leverage kube‑adm config files. Instead of long CLI flags, create a ClusterConfiguration.yaml and run kubeadm init --config=ClusterConfiguration.yaml for reproducibility.
• Monitor with Prometheus. Deploy the kube‑prometheus‑stack helm chart early to catch performance bottlenecks before they impact production.
Frequently Asked Questions
Can I run Kubernetes on a single bare‑metal server?
Yes, for learning or small‑scale CI you can run a single‑node cluster by adding --single-node to kubeadm init. However, you lose high‑availability guarantees and cannot test multi‑node networking.
Do I need to install a separate load balancer?
For a production control plane you should front the API server with a load balancer (e.g., HAProxy or MetalLB) that distributes traffic across multiple master nodes. A single‑node master works for labs but is a single point of failure.
How do I upgrade the cluster to a newer Kubernetes version?
Upgrade in three phases: (1) drain a control‑plane node, (2) upgrade kubeadm and kubelet packages, (3) run kubeadm upgrade apply vX.Y.Z. Repeat for each master, then upgrade workers. Always test the upgrade on a staging environment first.
Conclusion
Setting up Kubernetes on bare metal may look daunting, but by following these systematic steps you gain a high‑performance, low‑latency platform that you control from the ground up. From hardware preparation to network plug‑in, each phase builds on the previous one, ensuring a stable and secure cluster ready for real‑world workloads. Keep an eye on common pitfalls, automate repetitive tasks, and you’ll have a production‑grade Kubernetes environment that scales with your ambitions.
Photo by Ian Talmacs on Unsplash




