PARAGUAY

TAIWAN

ALBANIA

ARGENTINA

AUSTRALIA

AUSTRIA

AZERBAIJAN

BANGLADESH

BELGIUM

BOSNIA AND HERZEGOVINA

BRAZIL

BULGARIA

CANADA

CHILE

CHINA

COLOMBIA

COSTA RICA

CROATIA

CYPRUS

CZECH

DENMARK

ECUADOR

EGYPT

ESTONIA

FINLAND

FRANCE

GEORGIA

GERMANY

GREECE

GUATEMALA

HUNGARY

ICELAND

IN AFRICA

IN ASIA

IN AUSTRALIA

IN EUROPE

IN NORTH AMERICA

IN SOUTH AMERICA

INDIA

INDONESIA

IRELAND

ISRAEL

ITALY

JAPAN

KAZAKHSTAN

KENYA

KOSOVO

LATVIA

LIBYA

LITHUANIA

LUXEMBOURG

MALAYSIA

MALTA

MEXICO

MOLDOVA

MONTENEGRO

MOROCCO

NETHERLANDS

NEW ZEALAND

NIGERIA

NORWAY

PAKISTAN

PANAMA

PERU

PHILIPPINES

POLAND

PORTUGAL

QATAR

ROMANIA

RUSSIA

SAUDI ARABIA

SERBIA

SINGAPORE

SLOVAKIA

SLOVENIA

SOUTH AFRICA

SOUTH KOREA

SPAIN

SWEDEN

SWITZERLAND

THAILAND

TUNISIA

TURKEY

UAE

UK

UKRAINE

URUGUAY

USA

UZBEKISTAN

VIETNAM

LOGIN

How to Deploy a Production-Ready Kubernetes Cluster on Bare Metal Servers

While managed cloud container services offer out-of-the-box convenience, scaling them often results in unpredictable bandwidth costs and restricted access to underlying hardware.

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

  • 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.

bash
 
# 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.

bash
 
# 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.

bash
 
# 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).

bash
 
# 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.

bash
 
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:

bash
 
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.

bash
 
# 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:

bash
 
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.

yaml
 
# 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:

bash
 
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.