DEV Community

Cover image for How to Deploy a Production-Ready Kubernetes Cluster on Bare Metal Servers
Felicia Grace for BytesRack

Posted on • Originally published at bytesrack.com

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.


⚡ 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

Note: 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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Step 2: Install and Configure Container Runtime (containerd)

Note: 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
Enter fullscreen mode Exit fullscreen mode

Step 3: Install Kubernetes Components

Note: 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](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/](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
Enter fullscreen mode Exit fullscreen mode

Step 4: Bootstrap the Control Plane

Note: 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 5: Deploy the Container Network Interface (Calico)

Note: 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](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](https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml)

Enter fullscreen mode Exit fullscreen mode

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

Note: 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](https://raw.githubusercontent.com/metallb/metallb/v0.14.3/config/manifests/metallb-native.yaml)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply the configuration:

kubectl apply -f metallb-config.yaml
Enter fullscreen mode Exit fullscreen mode

Your bare metal Kubernetes cluster is now fully functional, networked, and capable of receiving external traffic!

Top comments (0)