Running Kubernetes on virtual machines or managed cloud services is convenient, but many enterprises prefer the raw performance and cost control of bare‑metal servers. This guide walks you through every phase of installing, configuring, and maintaining a production‑grade Kubernetes cluster on physical hardware. By the end you’ll have a resilient, network‑aware cluster ready for real workloads, plus a toolbox of best‑practice tips to keep it healthy.
What You’ll Need
- At least three identical servers (CPU, RAM, storage) – one for the control plane and two for workers.
- Ubuntu 22.04 LTS or CentOS Stream 9 (both are well‑supported by kubeadm).
- Root or sudo access on every node.
- A reliable out‑of‑band management interface (IPMI/iDRAC) for firmware updates.
- Static IP addresses for all nodes (or DHCP reservations).
- Basic networking knowledge – you’ll be configuring a pod network and possibly BGP.
- Internet connectivity for package installation and image pulls.
Step 1: Prepare the Physical Nodes
Before touching the OS, verify that each server’s BIOS/UEFI settings are optimized for Kubernetes:
- Enable VT‑x/AMD‑V for hardware virtualization.
- Disable Secure Boot (or enroll the Kubernetes signing keys).
- Turn on Power Management features that allow graceful shutdowns.
- Set the boot order to prioritize the local disk.
After BIOS tweaks, power on each node and confirm they can ping each other using their static IPs. Consistent hostname resolution (via /etc/hosts or DNS) will save you headaches later.
Step 2: Install a Linux OS and Harden It
We’ll use Ubuntu 22.04 LTS for illustration. Install the OS with the default partition layout, but consider a separate /var/lib/kubelet partition if you plan to host large container images.
# Example Ubuntu installation steps (run on a bootable USB)
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl gnupg2 lsb-release
Hardening steps:
- Disable swap – Kubernetes refuses to start with swap enabled.
- Set
net.bridge.bridge-nf-call-iptables=1and related sysctl values. - Configure a firewall that allows only required ports (see the Kubernetes port matrix).
# Disable swap permanently
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# Load kernel modules
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
br_netfilter
overlay
EOF
sudo modprobe br_netfilter
sudo modprobe overlay
# Sysctl settings
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system
Step 3: Set Up Networking (Calico or Flannel)
Kubernetes itself only provides a flat network for pods; you need a CNI plugin to enforce policies and route traffic. Calico is a popular choice for bare‑metal because it supports BGP peering and network‑policy enforcement without an overlay.
We’ll install Calico after the control plane is up, but you can pre‑download the manifest:
curl -O https://projectcalico.docs.tigera.io/manifests/calico.yaml
If you prefer a simpler overlay, replace the URL with the Flannel manifest.
Step 4: Install kubeadm, kubelet, and kubectl
All three components must be the same version on every node. Use the official Kubernetes apt repository:
# Add Google’s apt repo key
sudo curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
# Add the repo
cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
sudo apt update
sudo apt install -y kubelet=1.28.2-00 kubeadm=1.28.2-00 kubectl=1.28.2-00
sudo apt-mark hold kubelet kubeadm kubectl
Verify the installation:
kubeadm version && kubelet --version && kubectl version --client
Step 5: Bootstrap the Control Plane
Pick one node to host the control plane. Run kubeadm init with a pod network CIDR that matches Calico (default 192.168.0.0/16) and specify the API server’s advertised address.
sudo kubeadm init
--apiserver-advertise-address=10.0.0.10
--pod-network-cidr=192.168.0.0/16
--control-plane-endpoint=k8s.example.com:6443
--upload-certs
After the command finishes, set up your user environment:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Run kubectl get nodes – you should see the control‑plane node in a Ready state.
Step 6: Join Worker Nodes
When kubeadm init completed, it printed a kubeadm join command that contains a token and a hash. If you missed it, generate a new one on the control plane:
sudo kubeadm token create --print-join-command
Execute the printed command on each worker node (run as root or with sudo). Example:
sudo kubeadm join 10.0.0.10:6443
--token abcdef.0123456789abcdef
--discovery-token-ca-cert-hash sha256:112233445566778899aabbccddeeff00112233445566778899aabbccddeeff
After a minute, return to the control plane and verify:
kubectl get nodes -o wide
Step 7: Deploy the Pod Network Add‑on
Now that the control plane and workers are talking, apply the Calico manifest you downloaded earlier:
kubectl apply -f calico.yaml
Watch the pods come up:
kubectl get pods -n kube-system -w
When all calico-node and calico-kube-controllers pods are Running, the cluster is ready for workloads.
Step 8: Validate the Cluster and Enable Add‑ons
Run a simple test deployment:
kubectl create deployment nginx --image=nginx:stable-alpine
kubectl expose deployment nginx --port=80 --type=NodePort
Find the assigned NodePort and curl it from any node:
kubectl get svc nginx
# Suppose NodePort is 31123
curl http://10.0.0.11:31123
If you see the default Nginx page, networking is working.
Consider enabling these common add‑ons for production:
- Metrics Server –
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml - Ingress Controller – NGINX Ingress or Traefik, depending on your traffic patterns.
- Helm – package manager for installing complex charts.
- Cluster Autoscaler – if you have spare capacity and want nodes to scale up/down automatically.
Common Mistakes to Avoid
Even seasoned engineers stumble on a few pitfalls when moving Kubernetes off the cloud:
- Leaving swap enabled. The kubelet will refuse to start, and the error log points to
--fail-swap-on. - Mismatched pod‑network CIDR. If the CIDR you pass to
kubeadm initdoesn’t match the CNI’s configuration, pods cannot communicate. - Firewall over‑restriction. Blocking ports 10250, 10255, 6443, or the CNI’s VXLAN/BGP ports will cause node‑to‑node failures.
- Using different Kubernetes versions across nodes. kubeadm will warn, but subtle API incompatibilities can break the cluster.
- Neglecting time synchronization. Install
chronyorntpdon every node; clock drift leads to certificate validation errors.
Tips and Tricks
These shortcuts help you run a smoother bare‑metal cluster:
- Use a local container registry. Set up
registry:2on a spare node and configure--insecure-registryor proper TLS to speed up image pulls. - Leverage IPMI to automate OS provisioning. Tools like
MAASorForemanlet you PXE‑boot and install Ubuntu on dozens of nodes with a single recipe. - Enable kubelet systemd drop‑in for cgroup driver alignment. Example:
# /etc/systemd/system/kubelet.service.d/10‑cgroup.conf
[Service]
Environment="KUBELET_CGROUP_ARGS=--cgroup-driver=systemd"
EOF
sudo systemctl daemon-reload && sudo systemctl restart kubelet
- Persist etcd backups. Schedule a cron job on the control plane to snapshot etcd daily:
ETCDCTL_API=3 etcdctl snapshot save /var/backups/etcd-$(date +%F).db
--endpoints=https://127.0.0.1:2379
--cacert=/etc/kubernetes/pki/etcd/ca.crt
--cert=/etc/kubernetes/pki/etcd/server.crt
--key=/etc/kubernetes/pki/etcd/server.key
Frequently Asked Questions
Can I run multiple control‑plane nodes on bare metal?
Yes. After the first control plane is up, you can add more by running kubeadm join with the --control-plane flag on each additional node. Remember to place a load balancer (HAProxy, Keepalived, or MetalLB) in front of the API servers.
Do I need to install Docker?
Kubernetes supports any OCI‑compatible runtime. On Ubuntu 22.04 the default is containerd, which kubeadm configures automatically. Installing Docker is optional and can add another layer of complexity.
How do I handle storage for stateful workloads?
On bare metal you typically deploy a CSI driver that talks to local disks (e.g., local-path-provisioner) or to a dedicated storage appliance (Ceph, OpenEBS, or Longhorn). The choice depends on redundancy requirements and performance goals.
Conclusion
Deploying Kubernetes on bare metal gives you the raw performance of your hardware while retaining the flexibility of cloud‑native orchestration. By following the eight steps above—preparing hardware, hardening the OS, installing the control plane, joining workers, and wiring a robust CNI—you’ll end up with a production‑ready cluster that can scale, self‑heal, and host complex micro‑service workloads. Keep an eye on the common mistakes, apply the tips, and regularly back up etcd. With those practices in place, your bare‑metal Kubernetes environment will be as reliable as any managed service, but with the cost and control advantages that only on‑premises infrastructure can provide.
Photo by Md Riduwan Molla on Unsplash




