Tools: How to Deploy a Production-Ready Kubernetes Cluster on Bare Metal Servers (2026)

Tools: How to Deploy a Production-Ready Kubernetes Cluster on Bare Metal Servers (2026)

⚡ Quick Summary

📋 Prerequisites

Step 1: Operating System and Network Preparation

Step 2: Install and Configure Container Runtime (containerd)

Step 3: Install Kubernetes Components

Step 4: Bootstrap the Control Plane

Step 5: Deploy the Container Network Interface (Calico)

Step 6: Join Additional Nodes to the Cluster

Step 7: Expose Services with MetalLB 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. To follow this tutorial, you will need the following infrastructure: 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. Next, load the necessary kernel modules and configure IPv4 forwarding. 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. Note: Run this step on ALL nodes. We will install kubeadm, kubelet, and kubectl using the official community-owned package repositories (pkgs.k8s.io). 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. 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: 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. Wait a few minutes, then run kubectl get nodes. Your primary control plane node should now report a Ready status. Note: Run the respective commands on your remaining nodes. Paste the kubeadm join commands generated in Step 4. 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: 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. Apply the configuration: Your bare metal Kubernetes cluster is now fully functional, networked, and capable of receiving external traffic! Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# Disable swap immediately -weight: 600;">sudo swapoff -a # Comment out the swap entry in /etc/fstab to persist across reboots -weight: 600;">sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab # Disable swap immediately -weight: 600;">sudo swapoff -a # Comment out the swap entry in /etc/fstab to persist across reboots -weight: 600;">sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab # Disable swap immediately -weight: 600;">sudo swapoff -a # Comment out the swap entry in /etc/fstab to persist across reboots -weight: 600;">sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab # Create the configuration file for containerd modules cat <<EOF | -weight: 600;">sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF -weight: 600;">sudo modprobe overlay -weight: 600;">sudo modprobe br_netfilter # Configure sysctl parameters for Kubernetes networking cat <<EOF | -weight: 600;">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 -weight: 600;">sudo sysctl --system # Create the configuration file for containerd modules cat <<EOF | -weight: 600;">sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF -weight: 600;">sudo modprobe overlay -weight: 600;">sudo modprobe br_netfilter # Configure sysctl parameters for Kubernetes networking cat <<EOF | -weight: 600;">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 -weight: 600;">sudo sysctl --system # Create the configuration file for containerd modules cat <<EOF | -weight: 600;">sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF -weight: 600;">sudo modprobe overlay -weight: 600;">sudo modprobe br_netfilter # Configure sysctl parameters for Kubernetes networking cat <<EOF | -weight: 600;">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 -weight: 600;">sudo sysctl --system # Install dependencies and containerd -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install -y ca-certificates -weight: 500;">curl gnupg containerd # Generate the default configuration file -weight: 600;">sudo mkdir -p /etc/containerd containerd config default | -weight: 600;">sudo tee /etc/containerd/config.toml > /dev/null # Configure containerd to use the systemd cgroup driver -weight: 600;">sudo sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml # Restart and -weight: 500;">enable containerd -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart containerd -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable containerd # Install dependencies and containerd -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install -y ca-certificates -weight: 500;">curl gnupg containerd # Generate the default configuration file -weight: 600;">sudo mkdir -p /etc/containerd containerd config default | -weight: 600;">sudo tee /etc/containerd/config.toml > /dev/null # Configure containerd to use the systemd cgroup driver -weight: 600;">sudo sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml # Restart and -weight: 500;">enable containerd -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart containerd -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable containerd # Install dependencies and containerd -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install -y ca-certificates -weight: 500;">curl gnupg containerd # Generate the default configuration file -weight: 600;">sudo mkdir -p /etc/containerd containerd config default | -weight: 600;">sudo tee /etc/containerd/config.toml > /dev/null # Configure containerd to use the systemd cgroup driver -weight: 600;">sudo sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml # Restart and -weight: 500;">enable containerd -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart containerd -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable containerd # Download the public signing key for the Kubernetes package repositories -weight: 500;">curl -fsSL [https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key](https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key) | -weight: 600;">sudo gpg --dearmor -o /etc/-weight: 500;">apt/keyrings/kubernetes--weight: 500;">apt-keyring.gpg # Add the appropriate Kubernetes -weight: 500;">apt repository echo 'deb [signed-by=/etc/-weight: 500;">apt/keyrings/kubernetes--weight: 500;">apt-keyring.gpg] [https://pkgs.k8s.io/core:/stable:/v1.29/deb/](https://pkgs.k8s.io/core:/stable:/v1.29/deb/) /' | -weight: 600;">sudo tee /etc/-weight: 500;">apt/sources.list.d/kubernetes.list # Update -weight: 500;">apt package index and -weight: 500;">install components -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install -y kubelet kubeadm -weight: 500;">kubectl # Pin the versions so they are not automatically upgraded -weight: 600;">sudo -weight: 500;">apt-mark hold kubelet kubeadm -weight: 500;">kubectl # Download the public signing key for the Kubernetes package repositories -weight: 500;">curl -fsSL [https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key](https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key) | -weight: 600;">sudo gpg --dearmor -o /etc/-weight: 500;">apt/keyrings/kubernetes--weight: 500;">apt-keyring.gpg # Add the appropriate Kubernetes -weight: 500;">apt repository echo 'deb [signed-by=/etc/-weight: 500;">apt/keyrings/kubernetes--weight: 500;">apt-keyring.gpg] [https://pkgs.k8s.io/core:/stable:/v1.29/deb/](https://pkgs.k8s.io/core:/stable:/v1.29/deb/) /' | -weight: 600;">sudo tee /etc/-weight: 500;">apt/sources.list.d/kubernetes.list # Update -weight: 500;">apt package index and -weight: 500;">install components -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install -y kubelet kubeadm -weight: 500;">kubectl # Pin the versions so they are not automatically upgraded -weight: 600;">sudo -weight: 500;">apt-mark hold kubelet kubeadm -weight: 500;">kubectl # Download the public signing key for the Kubernetes package repositories -weight: 500;">curl -fsSL [https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key](https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key) | -weight: 600;">sudo gpg --dearmor -o /etc/-weight: 500;">apt/keyrings/kubernetes--weight: 500;">apt-keyring.gpg # Add the appropriate Kubernetes -weight: 500;">apt repository echo 'deb [signed-by=/etc/-weight: 500;">apt/keyrings/kubernetes--weight: 500;">apt-keyring.gpg] [https://pkgs.k8s.io/core:/stable:/v1.29/deb/](https://pkgs.k8s.io/core:/stable:/v1.29/deb/) /' | -weight: 600;">sudo tee /etc/-weight: 500;">apt/sources.list.d/kubernetes.list # Update -weight: 500;">apt package index and -weight: 500;">install components -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install -y kubelet kubeadm -weight: 500;">kubectl # Pin the versions so they are not automatically upgraded -weight: 600;">sudo -weight: 500;">apt-mark hold kubelet kubeadm -weight: 500;">kubectl -weight: 600;">sudo kubeadm init \ --control-plane-endpoint="<LOAD_BALANCER_IP>:6443" \ --upload-certs \ --pod-network-cidr=192.168.0.0/16 -weight: 600;">sudo kubeadm init \ --control-plane-endpoint="<LOAD_BALANCER_IP>:6443" \ --upload-certs \ --pod-network-cidr=192.168.0.0/16 -weight: 600;">sudo kubeadm init \ --control-plane-endpoint="<LOAD_BALANCER_IP>:6443" \ --upload-certs \ --pod-network-cidr=192.168.0.0/16 mkdir -p $HOME/.kube -weight: 600;">sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config -weight: 600;">sudo chown $(id -u):$(id -g) $HOME/.kube/config mkdir -p $HOME/.kube -weight: 600;">sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config -weight: 600;">sudo chown $(id -u):$(id -g) $HOME/.kube/config mkdir -p $HOME/.kube -weight: 600;">sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config -weight: 600;">sudo chown $(id -u):$(id -g) $HOME/.kube/config # Install the Tigera operator -weight: 500;">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) -weight: 500;">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) # Install the Tigera operator -weight: 500;">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) -weight: 500;">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) # Install the Tigera operator -weight: 500;">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) -weight: 500;">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) -weight: 500;">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) -weight: 500;">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) -weight: 500;">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) # 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 # 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 # 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 -weight: 500;">kubectl apply -f metallb-config.yaml -weight: 500;">kubectl apply -f metallb-config.yaml -weight: 500;">kubectl apply -f metallb-config.yaml - 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. - 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 -weight: 600;">sudo privileges on all machines. - 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).