Skip to content
Back to Blog
Linux & Server12 min read

Kubernetes Cluster Setup: 5 Steps to Production for Beginners

Build your first Kubernetes cluster from scratch with five concrete steps that take you from nothing to a working production environment.

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

Kubernetes looks complicated until you break it down. I'll walk you through a working cluster setup that you can run in production, starting from absolute zero. No experience required.

What is Kubernetes and why does it matter

Kubernetes orchestrates containers. That's it. Instead of manually starting containers on different servers and restarting them when they crash, Kubernetes does it automatically. You describe what you want running, and the system keeps it that way.

A cluster is a group of machines working together. One or more control plane nodes manage the cluster. Worker nodes run your actual applications. Each application runs inside a pod, which is one or more containers that share networking and storage.

The control plane makes scheduling decisions, tracks cluster state, and responds to events. The kubelet agent on each worker node talks to the control plane and manages containers locally. Everything communicates through the API server.

You'll interact with your cluster using kubectl, a command-line tool that sends instructions to the API server.

Step 1: Install container runtime and kubeadm

Every node needs a container runtime. Kubernetes stopped supporting Docker directly; containerd is now the standard choice. Install it on every machine that will join your cluster.

On Ubuntu or Debian:

sudo apt-get update
sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd

Next, add the Kubernetes package repository and install three tools:

sudo apt-get install -y apt-transport-https ca-certificates curl
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo 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.28/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

kubeadm bootstraps the cluster. kubelet runs on every node and manages containers. kubectl is your command-line interface.

The apt-mark hold line prevents accidental upgrades that could break your cluster. Version mismatches between nodes cause weird failures.

Disable swap on all nodes—Kubernetes requires it:

sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

Load required kernel modules:

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

Set sysctl parameters:

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

sudo sysctl --system

These network settings allow containers to communicate across nodes.

Step 2: Initialize the control plane

Pick one machine to be your first control plane node. Run this command there:

sudo kubeadm init --pod-network-cidr=10.244.0.0/16

The --pod-network-cidr flag reserves an IP range for pod networking. This specific range matches the default for Flannel, a popular network plugin we'll install shortly.

The init process takes a few minutes. It generates certificates, starts control plane components, and configures the API server. When it finishes, you'll see a join command that looks like this:

kubeadm join 192.168.1.10:6443 --token abc123.def456ghi789 \
    --discovery-token-ca-cert-hash sha256:1234567890abcdef...

Save that command. You'll run it on every worker node.

Before anything works, configure kubectl for your user:

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

Now test it:

kubectl get nodes

You'll see one node in NotReady state. That's expected—pods can't run until you install a network plugin.

Step 3: Deploy pod networking

Pods need to communicate across nodes. A CNI plugin (Container Network Interface) creates a virtual network for your cluster. Without it, nothing runs.

Flannel is simple and reliable for most setups:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

Watch the system pods start:

kubectl get pods -n kube-system -w

After a minute, everything should show Running. Check your node status again:

kubectl get nodes

The control plane node now shows Ready. Your cluster works.

Alternatives to Flannel include Calico (more features, steeper learning curve) and Cilium (eBPF-based, excellent observability). For your first cluster, stick with Flannel.

Step 4: Add worker nodes

Worker nodes run your applications. The control plane makes decisions; workers execute them.

On each worker machine, install containerd and the Kubernetes tools exactly as you did in step 1. Then run the join command you saved earlier:

sudo kubeadm join 192.168.1.10:6443 --token abc123.def456ghi789 \
    --discovery-token-ca-cert-hash sha256:1234567890abcdef...

The join command includes a token that expires after 24 hours. If your token expired, generate a new one on the control plane:

kubeadm token create --print-join-command

After joining, verify the node from your control plane:

kubectl get nodes

Each worker takes about a minute to become Ready while the kubelet starts and registers with the control plane.

You need at least one worker node for production. Three gives you redundancy—if one fails, workloads reschedule automatically to the others.

What about actually running something

You've built infrastructure. Now test it with a real workload.

Create a simple nginx deployment:

kubectl create deployment nginx --image=nginx:latest --replicas=3

This tells Kubernetes to run three nginx pods. Watch them start:

kubectl get pods -o wide

The -o wide flag shows which node each pod landed on. Kubernetes automatically spreads them across available workers.

Expose the deployment with a service:

kubectl expose deployment nginx --port=80 --type=NodePort

Find the assigned port:

kubectl get service nginx

You'll see something like 80:30123/TCP. That means nginx is accessible on port 30123 of any node IP. Test it:

curl http://<any-node-ip>:30123

You should get the nginx welcome page.

To remove the test deployment:

kubectl delete deployment nginx
kubectl delete service nginx

Step 5: Secure and prepare for production

A working cluster isn't a production cluster. You need to lock down access, set resource limits, and plan for failures.

Control access with RBAC

The admin config you copied earlier gives unrestricted access. Create limited accounts for applications and team members.

Kubernetes uses Role-Based Access Control. You define roles with specific permissions, then bind them to users or service accounts.

Here's a read-only role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: default
subjects:
- kind: User
  name: developer
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Save that to rbac.yaml and apply it:

kubectl apply -f rbac.yaml

Never give cluster-admin rights to applications.

Set resource requests and limits

Without limits, a single pod can starve everything else. Every production deployment should specify CPU and memory:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        resources:
          requests:
            memory: "128Mi"
            cpu: "250m"
          limits:
            memory: "256Mi"
            cpu: "500m"

Requests tell the scheduler how much the pod needs. Limits cap what it can use. The scheduler won't place a pod on a node that can't meet its requests.

Back up etcd regularly

Your cluster state lives in etcd, a key-value store running on the control plane. Lose etcd and you lose everything.

Snapshot it regularly:

sudo ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot.db \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key

Automate this with a cron job and copy snapshots off the cluster.

Monitor control plane certificates

Kubernetes generates certificates that expire after one year. Check expiration dates:

sudo kubeadm certs check-expiration

Renew before they expire:

sudo kubeadm certs renew all

After renewal, restart control plane pods:

sudo systemctl restart kubelet

Use a load balancer for high availability

A single control plane is a single point of failure. Production clusters should run three control plane nodes behind a load balancer.

Initialize the cluster with the load balancer address:

sudo kubeadm init --control-plane-endpoint "loadbalancer.example.com:6443" --upload-certs

Then join additional control plane nodes using the special join command that kubeadm prints.

Common problems and fixes

Pods stuck in Pending

Check if the scheduler can find a suitable node:

kubectl describe pod <pod-name>

Look at the Events section. Common causes: insufficient CPU or memory, no nodes match the pod's requirements, or the network plugin isn't running.

Node shows NotReady

The kubelet might have stopped:

sudo systemctl status kubelet
sudo journalctl -u kubelet -f

Restart it if needed:

sudo systemctl restart kubelet

Can't connect to the API server

Verify the control plane pods are running:

sudo crictl ps

If API server pods keep restarting, check certificate validity and ensure etcd is healthy.

DNS resolution fails inside pods

Kubernetes runs CoreDNS for cluster DNS. Check if it's working:

kubectl get pods -n kube-system -l k8s-app=kube-dns

If CoreDNS pods are failing, check CNI plugin status and node resources.

What to check before going live

You built a cluster. Here's your pre-launch checklist:

  • Run three control plane nodes with a load balancer.
  • Set resource requests and limits on every deployment.
  • Configure RBAC and remove cluster-admin access from applications.
  • Automate etcd backups and test restoration.
  • Monitor certificate expiration dates.
  • Document your pod network CIDR and service CIDR ranges.
  • Set up monitoring (Prometheus and Grafana are standard).
  • Test node failure scenarios—kill a node and verify workloads reschedule.

Kubernetes is now a skill you have. Start small, deploy real applications, and learn from what breaks. The first cluster teaches you more than any documentation.

FAQ

How many nodes do I need?

One control plane and two workers is the practical minimum for production. Three control planes and three workers give you real high availability.

Can I run the control plane and workers on the same node?

Yes, remove the NoSchedule taint: kubectl taint nodes --all node-role.kubernetes.io/control-plane-. Fine for testing, risky for production.

What if I need to reset and start over?

Run sudo kubeadm reset on all nodes, then begin again from step 2. You'll lose all cluster data.

How do I upgrade Kubernetes versions?

Upgrade kubeadm first, then drain and upgrade nodes one at a time. The kubeadm documentation has version-specific instructions.

Do I need to know Docker?

Helpful but not required. Kubernetes abstracts most container details. You'll work with pod definitions, not raw container commands.