Orchestrating containers at scale is the cornerstone of modern cloud‑native architectures, and Kubernetes has become the de‑facto platform for that job. This guide assumes you already know Docker basics and are comfortable with Linux command‑line tools. We’ll walk through building a production‑ready multi‑node cluster, wiring up networking, deploying a real‑world app, and hardening security—all with concrete commands you can copy‑paste.
What You’ll Need
- At least three Linux VMs (Ubuntu 22.04 LTS recommended) – one master and two workers.
- Root or sudo access on each node.
- Minimum 2 CPU and 4 GB RAM per node.
- Static IPs or a DHCP reservation for each VM.
- kubectl installed locally (or on the master).
Step 1: Set Up a Multi‑Node Cluster
We’ll use kubeadm because it gives you full control over each component. Begin by disabling swap on every node (Kubernetes won’t start otherwise):sudo swapoff -a && sudo sed -i '/ swap / s/^/#/' /etc/fstab
Next, install the required packages:
sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/kubernetes-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | 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 On the master, initialise the control plane (replace --apiserver-advertise-address with your master’s IP):
sudo kubeadm init --apiserver-advertise-address=192.168.1.10 --pod-network-cidr=192.168.0.0/16 After a successful init, set up your local kubeconfig:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config Now join each worker node using the token printed at the end of kubeadm init. If you missed it, generate a new one:
sudo kubeadm token create --print-join-command Run the resulting kubeadm join command on each worker. Verify the cluster is healthy:
kubectl get nodes You should see all three nodes in a Ready state.
Step 2: Install a Container Runtime
Kubernetes 1.24+ dropped Docker as a supported runtime, so we’ll use containerd. Install it on every node:
sudo apt-get install -y containerd
sudo systemctl enable --now containerd Configure the default runtime class (optional but recommended for consistency):
cat <<EOF | sudo tee /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
EOF
sudo systemctl restart containerd After restarting, confirm Docker‑style commands still work:
sudo crictl info If you see JSON output, you’re good to go.
Step 3: Deploy a Sample Application
Let’s spin up a classic nginx deployment with a Service and an Ingress. First, create a namespace to keep things tidy:
kubectl create namespace demo Now apply the manifest:
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
namespace: demo
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
namespace: demo
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
namespace: demo
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: nginx.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-svc
port:
number: 80
EOF Check that pods are running:
kubectl get pods -n demo -o wide Because we haven’t installed an Ingress controller yet, the Ingress resource will stay pending. We’ll fix that in the next step.
Step 4: Configure Networking with Calico
While the --pod-network-cidr=192.168.0.0/16 flag prepared the CIDR, we still need a CNI plugin. Calico is a popular choice for its network policy capabilities. Install it on the master (it propagates to workers):
kubectl apply -f https://projectcalico.docs.tigera.io/manifests/calico.yaml Wait for all Calico pods to become Running:
kubectl get pods -n kube-system -l k8s-app=calico-node Now install an NGINX Ingress controller (also a Calico‑compatible deployment):
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml After the controller is ready, add an entry to your local /etc/hosts (or DNS) so nginx.local resolves to the Ingress IP:
# Get external IP (or use the node IP for minikube)
kubectl get svc -n ingress-nginx ingress-nginx-controller
# Example entry
echo "192.168.1.10 nginx.local" | sudo tee -a /etc/hosts Now you can curl the service:
curl http://nginx.local You should see the default NGINX welcome page, confirming that networking, Service, and Ingress are all wired correctly.
Step 5: Implement Autoscaling
Kubernetes offers two autoscaling mechanisms: the Horizontal Pod Autoscaler (HPA) for workloads and the Cluster Autoscaler for node pools. First, enable the metrics server (required for HPA):
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml Verify it’s serving metrics:
kubectl top nodes Now create an HPA that scales the NGINX deployment based on CPU usage:
kubectl autoscale deployment nginx-deploy
--cpu-percent=50 --min=2 --max=6 -n demo Generate load to see it in action (run from any machine that can reach the Ingress):
while true; do curl -s http://nginx.local > /dev/null; done In another terminal, watch the HPA:
kubectl get hpa -n demo When CPU crosses 50 %, the replica count will rise up to six. For node‑level autoscaling, install the Cluster Autoscaler (the exact manifest varies by cloud provider; for bare‑metal you can use the open‑source version with custom node‑group definitions).
Step 6: Secure the Cluster with RBAC
Role‑Based Access Control (RBAC) is the default security model, but you still need to create least‑privilege roles for developers and CI pipelines. Let’s create a read‑only role for the demo namespace:
cat <<'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: demo
name: demo-viewer
rules:
- apiGroups: ["", "apps", "extensions"]
resources: ["pods", "deployments", "services", "ingresses"]
verbs: ["get", "list", "watch"]
EOF
# Bind the role to a user (replace USERNAME with your IdP user)
cat <<'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: demo-viewer-binding
namespace: demo
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: demo-viewer
apiGroup: rbac.authorization.k8s.io
EOF Test the restriction by impersonating the user:
kubectl auth can-i create deployments -n demo --as=alice@example.com The command should return no, confirming the role is correctly scoped.
Common Mistakes to Avoid
1. Skipping swap disable. Even a tiny swap file will cause kubelet to refuse to start.
2. Mismatched pod CIDR and CNI config. If the CIDR you pass to kubeadm init doesn’t match Calico’s clusterCIDR, pods won’t get IPs.
3. Using Docker as the runtime on newer clusters. Kubernetes 1.24+ expects a CRI‑compatible runtime like containerd.
4. Forgetting to open required ports. Ports 6443, 10250, 30000‑32767 (NodePort) and the CNI overlay ports must be reachable between nodes.
5. Applying manifests without namespace isolation. Deploying everything into default makes cleanup messy and can cause naming collisions.
6. Neglecting metrics‑server. HPA will silently stay at the minimum replica count if metrics aren’t available.
Tips and Tricks
• Use kubeadm reset on a node before re‑joining it to avoid “node already exists” errors.
• Store your kubeconfig in a version‑controlled .kube directory with separate contexts for each cluster.
• Leverage kubectl kustomize to templatize environment‑specific values (dev, staging, prod).
• Enable audit logging on the API server for compliance: add --audit-log-path=/var/log/kubernetes/audit.log to the API server manifest.
• When using Calico network policies, start with a “default deny all” policy and then whitelist required traffic to reduce blast‑radius of compromised pods.
Frequently Asked Questions
Can I run Kubernetes on a single VM for testing?
Yes. Tools like kind (Kubernetes IN Docker) or minikube spin up a single‑node cluster quickly, but they hide many production‑level steps such as CNI installation and RBAC configuration.
Do I need a separate load balancer for the Ingress controller?
On bare‑metal you can expose the NGINX Ingress controller via a NodePort or HostPort. For cloud deployments, attach a cloud‑provider LB (AWS ELB, GCP LB) to the Ingress Service of type LoadBalancer for high‑availability.
How often should I upgrade my cluster?
Kubernetes releases a new minor version roughly every three months. Aim to upgrade at least once per quarter to stay on supported versions, benefit from security patches, and avoid painful version gaps.
Conclusion
By following this guide you’ve built a robust, production‑grade Kubernetes cluster, wired up networking, deployed a real application, and layered in autoscaling and security. The real power of Kubernetes shines when you start integrating CI/CD pipelines, service meshes, and observability stacks. Keep experimenting, automate the repetitive parts with Terraform or Ansible, and remember that the best way to master orchestration is to keep breaking and fixing things in a sandbox before you ship to production.




