Skip to content
Back to Blog
Linux & Server11 min read

Kubernetes Cluster Setup: 5 Steps to Production in 2026

Build a production-ready Kubernetes cluster from scratch with practical configurations, real troubleshooting steps, and battle-tested deployment patterns.

Written by Abdul AbrorTechnical Hosting Support Engineer
Kubernetes Cluster Setup: 5 Steps to Production in 2026
On this page

Running containers in production means dealing with orchestration, and Kubernetes remains the standard choice. I've walked through dozens of cluster setups for clients moving from Docker Compose or manual container management to a proper orchestration platform. The jump feels steep at first, but the process breaks down into five repeatable steps that work whether you're building a three-node cluster in a data center or a managed setup in the cloud.

This guide covers the full path from choosing your infrastructure to serving live traffic, with the configuration files and troubleshooting commands I actually use.

Step 1: Choose your control plane architecture

Your control plane runs the API server, scheduler, and controller manager. For production, you need at least three control plane nodes to survive a failure. A single control plane node is fine for development or staging, but one hardware fault takes down your entire cluster.

I run control plane nodes on separate machines from worker nodes. Some teams collapse everything onto the same hardware to save costs, but that creates scheduling conflicts and makes upgrades riskier. Keep them separate if you can.

The minimum viable production cluster looks like this: - Three control plane nodes (2 CPU, 4GB RAM each) - Three worker nodes (4 CPU, 8GB RAM each, scaled up based on workload) - An external etcd cluster or embedded etcd on control plane nodes

For etcd, embedded mode is simpler to manage. External etcd gives you more isolation if you need to snapshot and restore the cluster state frequently, but it adds three more machines to maintain.

Step 2: Install and bootstrap with kubeadm

Kubeadm handles the heavy lifting of certificate generation, component configuration, and node joining. Other tools exist—kubespray, k3s, managed services—but kubeadm gives you the most control over the result and works on any Linux distribution.

On your first control plane node, install the runtime and Kubernetes packages:

# Install containerd
apt-get update
apt-get install -y containerd
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml

# Enable systemd cgroup driver
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
systemctl restart containerd

# Install kubeadm, kubelet, kubectl
apt-get install -y apt-transport-https ca-certificates curl
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' > /etc/apt/sources.list.d/kubernetes.list
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

Now initialize the first control plane node:

kubeadm init --control-plane-endpoint="loadbalancer.example.com:6443" \
  --upload-certs \
  --pod-network-cidr=10.244.0.0/16

The --control-plane-endpoint should point to a load balancer in front of your control plane nodes. If you don't have one yet, use the IP of this first node and add a load balancer later—just know you'll need to update kubeconfigs and certificates when you do.

Save the join commands kubeadm prints. You'll need them to add control plane and worker nodes.

Copy the admin kubeconfig:

mkdir -p $HOME/.kube
cp /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

Step 3: Configure pod networking

Kubernetes doesn't ship with a network plugin. Pods can't communicate across nodes until you install one. Calico and Cilium are the two I deploy most often.

Calico is straightforward and works everywhere:

kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

Cilium gives you eBPF-based networking and better observability, but it requires a recent kernel:

helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --namespace kube-system

After applying the network plugin, watch the CoreDNS pods come up:

kubectl get pods -n kube-system -w

They should transition to Running within a minute. If they stay Pending, check that your pod network CIDR doesn't overlap with your node network.

What if pods stay in ContainerCreating?

Nine times out of ten, it's the CNI plugin not finishing its configuration. Check the node's CNI config:

ls /etc/cni/net.d/

You should see a 10-calico.conflist or similar file. If it's missing, the network plugin didn't install correctly. Check the calico-node or cilium-agent logs:

kubectl logs -n kube-system -l k8s-app=calico-node

Common issues include missing kernel modules (check lsmod | grep ip_tables) or firewall rules blocking VXLAN or BGP traffic.

Step 4: Add nodes and configure storage

Join additional control plane nodes with the command kubeadm printed earlier:

kubeadm join loadbalancer.example.com:6443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash> \
  --control-plane --certificate-key <key>

Join worker nodes without the --control-plane flag:

kubeadm join loadbalancer.example.com:6443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

If you lost the join command, generate a new token:

kubeadm token create --print-join-command

Once nodes are joined, verify:

kubectl get nodes

All nodes should show Ready status.

For persistent storage, you need a StorageClass. If you're running on bare metal, use local-path-provisioner for simple cases or Longhorn for replicated block storage. On cloud providers, use the CSI driver for your platform.

Local-path example:

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

Now PersistentVolumeClaims will automatically provision volumes on worker node disks.

Step 5: Deploy your first workload and expose it

Start with a simple deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 200m
            memory: 256Mi

Apply it:

kubectl apply -f nginx-demo.yaml
kubectl get pods -l app=nginx

Expose the deployment with a Service:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: LoadBalancer

On cloud platforms, type: LoadBalancer provisions an external load balancer automatically. On bare metal, you'll need MetalLB or an external load balancer.

For HTTP/HTTPS traffic, install an Ingress controller instead. NGINX Ingress is the most widely deployed:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/baremetal/deploy.yaml

Create an Ingress resource:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-service
            port:
              number: 80

Point your DNS at the Ingress controller's external IP:

kubectl get svc -n ingress-nginx

How do I debug when a service is unreachable?

Start inside the cluster. Exec into a pod and try to reach the service by its ClusterIP:

kubectl run -it --rm debug --image=busybox --restart=Never -- sh
wget -O- http://nginx-service

If that works, the problem is in your Ingress or external routing. If it doesn't, check endpoints:

kubectl get endpoints nginx-service

Empty endpoints mean the Service selector doesn't match any pods. Check your labels:

kubectl get pods --show-labels

If endpoints exist but traffic doesn't reach the pods, check network policies and CNI logs.

Hardening and day-two operations

Once the cluster is running, lock it down. Disable anonymous auth and enable audit logging in /etc/kubernetes/manifests/kube-apiserver.yaml:

- --anonymous-auth=false
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30

Restrict pod permissions with PodSecurityStandards. Create a namespace with restricted enforcement:

kubectl create namespace production
kubectl label namespace production pod-security.kubernetes.io/enforce=restricted

Set up monitoring with Prometheus and Grafana. The kube-prometheus-stack Helm chart bundles everything:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --namespace monitoring --create-namespace

For backups, snapshot etcd regularly:

ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot-$(date +%Y%m%d).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

Store snapshots off-cluster. I've seen teams lose clusters and backups at the same time because they kept snapshots on the same disk.

Common questions

How many control plane nodes do I actually need?
Three is the minimum for high availability. Five gives you more fault tolerance but increases etcd write latency. Stick with three unless you're running a massive cluster.

Can I mix cloud and on-premises nodes?
Yes, but networking gets complex. You'll need a VPN or WireGuard mesh to let pods communicate across environments. Cluster API providers can help automate this.

What's the difference between a DaemonSet and a Deployment?
A DaemonSet runs one pod per node, usually for logging or monitoring agents. A Deployment runs a specific number of replicas scheduled anywhere in the cluster.

How do I upgrade the cluster?
Upgrade control plane nodes one at a time with kubeadm upgrade apply, then upgrade workers with kubeadm upgrade node. Always upgrade in minor version steps—don't skip versions.

Should I use Helm for everything?
Helm simplifies complex applications with lots of templating, but plain YAML is easier to read and debug. Use Helm for third-party charts, plain manifests for your own apps.

What to monitor after deployment

Watch these metrics daily in your Prometheus dashboards: node CPU and memory pressure, pod restart counts, persistent volume usage, and API server request latency. High restart counts usually mean your resource limits are too low or your app is crashing. API server latency spikes indicate etcd performance problems or too many watchers.

Set alerts for node NotReady transitions and pod evictions. Both signal capacity or stability issues that need immediate attention.

The first production cluster always feels overwhelming. Start small, add nodes as load grows, and keep your manifests in git so you can recreate the setup if disaster strikes.