By migrating your container infrastructure to bare metal, you eliminate virtualization overhead, regain complete control over your network topology, and maximize compute efficiency.
This tutorial provides a complete guide to deploying a highly available, production-ready Kubernetes (K8s) cluster directly on bare metal servers. We will configure the containerd runtime, bootstrap the cluster via kubeadm, deploy Calico for secure pod networking, and implement MetalLB to handle external load balancing.
What You'll Learn
Quick Summary
Prerequisites
Step 1: Operating System and Network Preparation
Step 2: Install and Configure Container Runtime
Step 3: Install Kubernetes Components
Step 4: Bootstrap the Control Plane
Step 5: Deploy the Container Network Interface (Calico)
Step 6: Join Additional Nodes
Step 7: Expose Services with MetalLB
Quick Summary
Prepare Nodes: Disable swap memory and load required kernel modules (overlay, br_netfilter) on all servers.
Install Runtime: Configure containerd with the systemd cgroup driver.
Bootstrap Cluster: Use kubeadm init with a highly available control-plane endpoint.
Establish Networking: Deploy the Calico Container Network Interface (CNI) for pod-to-pod communication.
Enable Ingress: Configure MetalLB to expose services to external networks, bridging the gap left by missing cloud-native load balancers.
Prerequisites
To follow this tutorial, you will need the following infrastructure:
Load Balancer / VIP: A pre-configured highly available IP (via HAProxy/Keepalived or kube-vip) pointing to your control plane nodes on port 6443.
Control Plane Nodes: 3x Ubuntu 22.04 or 24.04 servers (Minimum 4 vCPU, 8GB RAM).
Worker Nodes: 2+ Ubuntu 22.04 or 24.04 servers (Minimum 4 vCPU, 16GB RAM).
Network: All nodes must communicate over a secure private network with static IPs.
Access: Full root or sudo privileges on all machines.
Step 1: Operating System and Network Preparation
Run this step on ALL nodes (Control Plane and Workers).
Kubernetes requires specific system configurations to route traffic and manage resources correctly. First, disable swap memory, as the kubelet will fail to start if swap is active.
# Disable swap immediately
sudo swapoff -a
# Comment out the swap entry in /etc/fstab to persist across reboots
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
Next, load the necessary kernel modules and configure IPv4 forwarding.
# Create the configuration file for containerd modules
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
# Configure sysctl parameters for Kubernetes networking
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
# Apply sysctl params without rebooting
sudo sysctl --system
Step 2: Install and Configure Container Runtime (containerd)
Run this step on ALL nodes.
Kubernetes deprecated Docker as a runtime in favor of Container Runtime Interface (CRI) compliant systems. We will install and configure containerd.
# Install dependencies and containerd
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg containerd
# Generate the default configuration file
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
# Configure containerd to use the systemd cgroup driver
sudo sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml
# Restart and enable containerd
sudo systemctl restart containerd
sudo systemctl enable containerd
Step 3: Install Kubernetes Components
Run this step on ALL nodes.
We will install kubeadm, kubelet, and kubectl using the official community-owned package repositories (pkgs.k8s.io).
# Download the public signing key for the Kubernetes package repositories
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# Add the appropriate Kubernetes apt repository
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
# Update apt package index and install components
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
# Pin the versions so they are not automatically upgraded
sudo apt-mark hold kubelet kubeadm kubectl
Step 4: Bootstrap the Control Plane
Run this step on your PRIMARY Control Plane Node ONLY.
Initialize the cluster using kubeadm. Replace <LOAD_BALANCER_IP> with the virtual IP or DNS name of your HA load balancer.
sudo kubeadm init \
--control-plane-endpoint="<LOAD_BALANCER_IP>:6443" \
--upload-certs \
--pod-network-cidr=192.168.0.0/16
Once the initialization completes, the terminal will output specific kubeadm join commands for both your remaining Control Plane nodes and your Worker nodes. Save these commands securely.
Configure your local kubectl to interact with the cluster:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Step 5: Deploy the Container Network Interface (Calico)
Run this step on your PRIMARY Control Plane Node ONLY.
Nodes will remain in a NotReady state until a CNI is installed. We will use Calico for its robust network policy engine.
# Install the Tigera operator
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
# Install the custom resources (this deploys Calico within the cluster)
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml
Wait a few minutes, then run kubectl get nodes. Your primary control plane node should now report a Ready status.
Step 6: Join Additional Nodes to the Cluster
Run the respective commands on your remaining nodes.
Paste the kubeadm join commands generated in Step 4.
For remaining Control Plane nodes: (The command will include --control-plane and a --certificate-key flag).
For Worker nodes: (The standard join command with the discovery token).
Step 7: Expose Services with MetalLB
Because bare metal environments lack the automated load balancers provided by managed cloud platforms (like AWS ELB), services defined as LoadBalancer will remain in a Pending state indefinitely. MetalLB solves this by allocating IPs from a designated pool directly to your cluster services.
Infrastructure Note: Routing Layer 2 broadcast traffic or assigning multiple dedicated IP blocks requires a hosting provider that does not restrict network topologies. This is where deploying your cluster on BytesRack's dedicated servers becomes a major advantage. BytesRack provides the raw network access and unmetered, high-throughput uplinks required by native tools like MetalLB, allowing you to seamlessly map public IPs to your Kubernetes services without provider-level firewall interference.
Install MetalLB natively:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.3/config/manifests/metallb-native.yaml
Create an IP Address Pool and an L2 Advertisement by applying the following YAML file. Replace the IP addresses with the block allocated to your servers.
# metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production-ip-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250 # Replace with your available IPs
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: production-l2-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production-ip-pool
Apply the configuration:
kubectl apply -f metallb-config.yaml
Your bare metal Kubernetes cluster is now fully functional, networked, and capable of receiving external traffic.
Conclusion
You have successfully constructed a highly available Kubernetes cluster on bare metal. By manually configuring containerd, deploying a CNI, and establishing an internal load balancing mechanism with MetalLB, you have built an enterprise-grade infrastructure that rivals managed cloud solutionsโwithout the vendor lock-in or premium bandwidth costs.
If you are looking to deploy this architecture on hardware designed for maximum uptime, BytesRack offers enterprise-grade dedicated servers equipped with high-speed NVMe storage, DDR5 memory, and unrestricted network access. Build your production K8s cluster on BytesRack's high-performance servers and gain ultimate control over your infrastructure.
Discover BytesRack Dedicated Server Locations
BytesRack servers are available around the world, providing diverse options for hosting websites. Each region offers unique advantages, making it easier to choose a location that best suits your specific hosting needs.

Media Stream Solutions
Gaming Solutions
E-Commerce Solutions
VPN Server Solutions
GPU Server Solutions
Financial Solutions
Security Solutions