Deploy a real app on Kubernetes from scratch — Deployment, Service, Ingress, ConfigMap, Secrets, rolling updates with rollback, and Horizontal Pod Autoscaling on a managed cluster.
Kubernetes has become the operating system of the cloud. Every major cloud provider offers a managed Kubernetes service — AWS EKS, Google GKE, Azure AKS — and the majority of production containerised workloads run on it. For Indian developers targeting product companies, startups with scale ambitions, or remote roles at international companies, Kubernetes knowledge is increasingly a baseline requirement, not a differentiator.
This guide demystifies Kubernetes by deploying a real Node.js application from scratch. You will write deployment manifests, expose the app via a Service and Ingress, manage configuration with ConfigMaps and Secrets, and configure Horizontal Pod Autoscaling. The guide uses kubectl commands that work identically on EKS, GKE, and any other conformant cluster.
| Concept | Analogy | What It Does |
|---|---|---|
| Pod | A running container (or containers) | Smallest deployable unit — one or more containers sharing network/storage |
| Deployment | A desired state declaration | Says 'I want 3 replicas of this Pod, always' — K8s maintains that |
| Service | A stable internal DNS name + load balancer | Routes traffic to Pods even as they restart and get new IPs |
| Ingress | Nginx/reverse proxy for the cluster | Routes external HTTP/S traffic to Services based on host/path rules |
| ConfigMap | Environment variables (non-secret) | Key-value config mounted into Pods as env vars or files |
| Secret | Environment variables (secret) | Base64-encoded sensitive values — database passwords, API keys |
| Namespace | A folder for resources | Logical isolation within a cluster — dev, staging, production |
| HPA | Auto-scaling policy | Scales Pod count up/down based on CPU or custom metrics |
GKE Autopilot manages node provisioning automatically — you only pay for the resources your Pods actually use, with no idle node costs. It includes a 90-day $300 free credit for new GCP accounts. It is the fastest path to a running cluster.
# Install gcloud CLI: https://cloud.google.com/sdk/docs/install
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
# Create Autopilot cluster in Mumbai region
gcloud container clusters create-auto my-cluster --region asia-south1
# Get kubectl credentials
gcloud container clusters get-credentials my-cluster --region asia-south1
# Verify connection
kubectl get nodes# Install eksctl: https://eksctl.io/installation/
# Install kubectl: https://kubernetes.io/docs/tasks/tools/
eksctl create cluster --name my-cluster --region ap-south-1 --nodegroup-name workers --node-type t3.medium --nodes 2 --nodes-min 1 --nodes-max 4 --managed
# Updates kubeconfig automatically
kubectl get nodes# Configure Docker to use gcloud credentials
gcloud auth configure-docker asia-south1-docker.pkg.dev
# Build and tag
docker build -t asia-south1-docker.pkg.dev/YOUR_PROJECT/my-app/api:v1 .
# Push
docker push asia-south1-docker.pkg.dev/YOUR_PROJECT/my-app/api:v1apiVersion: v1
kind: Namespace
metadata:
name: my-app
labels:
app: my-appkubectl apply -f k8s/namespace.yaml
# Set as default so you don't have to type -n my-app every time
kubectl config set-context --current --namespace=my-appapiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
namespace: my-app
data:
NODE_ENV: "production"
PORT: "3000"
AWS_REGION: "ap-south-1"
LOG_LEVEL: "info"# Create Secret imperatively (values are base64-encoded automatically)
kubectl create secret generic my-app-secrets --namespace my-app --from-literal=DATABASE_URL="postgresql://user:pass@host:5432/db" --from-literal=JWT_ACCESS_SECRET="$(openssl rand -hex 32)" --from-literal=JWT_REFRESH_SECRET="$(openssl rand -hex 32)"
# View secrets (values are base64 encoded, not encrypted by default)
kubectl get secret my-app-secrets -o yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-app
labels:
app: my-app
spec:
replicas: 2
selector:
matchLabels:
app: my-app
# Rolling update: bring up new pods before removing old ones
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # allow 1 extra pod during update
maxUnavailable: 0 # never reduce below desired replicas
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: api
image: asia-south1-docker.pkg.dev/YOUR_PROJECT/my-app/api:v1
ports:
- containerPort: 3000
# Load non-secret config from ConfigMap
envFrom:
- configMapRef:
name: my-app-config
# Load secrets individually
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: my-app-secrets
key: DATABASE_URL
- name: JWT_ACCESS_SECRET
valueFrom:
secretKeyRef:
name: my-app-secrets
key: JWT_ACCESS_SECRET
- name: JWT_REFRESH_SECRET
valueFrom:
secretKeyRef:
name: my-app-secrets
key: JWT_REFRESH_SECRET
# Resource requests and limits
resources:
requests:
cpu: "100m" # 0.1 CPU core
memory: "128Mi"
limits:
cpu: "500m" # 0.5 CPU core
memory: "512Mi"
# Liveness probe: restart pod if it stops responding
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
failureThreshold: 3
# Readiness probe: don't send traffic until app is ready
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3apiVersion: v1
kind: Service
metadata:
name: my-app-service
namespace: my-app
spec:
# ClusterIP: internal only (use with Ingress)
# LoadBalancer: creates a cloud load balancer (costs money, use sparingly)
type: ClusterIP
selector:
app: my-app # routes to pods with this label
ports:
- protocol: TCP
port: 80 # Service port
targetPort: 3000 # Pod portThe Ingress resource routes external HTTP/S traffic to Services. It requires an Ingress Controller to be installed in the cluster — the most common is Nginx Ingress Controller. GKE has a built-in GCE Ingress Controller; EKS uses the AWS Load Balancer Controller.
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/cloud/deploy.yaml
# Wait for it to get an external IP
kubectl get service ingress-nginx-controller -n ingress-nginx --watchapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: my-app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
# TLS termination — cert-manager will provision the certificate
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.yourdomain.com
secretName: my-app-tls
rules:
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 80# Apply all manifests
kubectl apply -f k8s/
# Watch pods come up
kubectl get pods --watch
# Check deployment status
kubectl rollout status deployment/my-app
# View logs
kubectl logs -l app=my-app --tail=50
# Describe a pod (useful for debugging CrashLoopBackOff)
kubectl describe pod <pod-name>
# Get external IP of Ingress
kubectl get ingress my-app-ingressRolling updates deploy new Pod versions gradually — new Pods start and pass readiness probes before old Pods are terminated. With maxUnavailable: 0, there is always the full desired replica count available during updates. Zero downtime is automatic.
# Build and push new image with new tag
docker build -t asia-south1-docker.pkg.dev/YOUR_PROJECT/my-app/api:v2 .
docker push asia-south1-docker.pkg.dev/YOUR_PROJECT/my-app/api:v2
# Update the deployment with new image
kubectl set image deployment/my-app api=asia-south1-docker.pkg.dev/YOUR_PROJECT/my-app/api:v2
# Watch the rolling update
kubectl rollout status deployment/my-app
# View rollout history
kubectl rollout history deployment/my-app
# Rollback to previous version (immediate)
kubectl rollout undo deployment/my-app
# Rollback to specific revision
kubectl rollout undo deployment/my-app --to-revision=2HPA automatically scales your Deployment's replica count based on CPU utilisation (or custom metrics). It requires the metrics-server to be installed in the cluster — GKE includes it by default; EKS requires a separate installation.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
namespace: my-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # scale up when avg CPU > 70%
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80# Apply HPA
kubectl apply -f k8s/hpa.yaml
# Watch scaling decisions
kubectl get hpa my-app-hpa --watch
# View current replica count and targets
kubectl describe hpa my-app-hpa| Command | What It Does |
|---|---|
| kubectl get pods | List all pods in current namespace |
| kubectl get pods -A | List pods across all namespaces |
| kubectl logs <pod> -f | Stream logs from a pod |
| kubectl exec -it <pod> -- sh | Open a shell inside a running pod |
| kubectl describe pod <pod> | Full details including events — first stop for debugging |
| kubectl get events --sort-by=.lastTimestamp | Recent cluster events — shows why pods fail |
| kubectl port-forward svc/my-app-service 8080:80 | Access service locally without Ingress |
| kubectl scale deployment/my-app --replicas=5 | Manually scale a deployment |
| kubectl delete pod <pod> | Delete a pod (Deployment recreates it immediately) |
| kubectl apply -f k8s/ | Apply all manifests in k8s/ directory |
| Symptom | Likely Cause | Fix |
|---|---|---|
| CrashLoopBackOff | App crashing on startup | kubectl logs <pod> — check error output |
| ImagePullBackOff | Can't pull container image | Check image name, tag, and registry credentials |
| Pending (forever) | Insufficient cluster resources | kubectl describe pod — check Events for scheduling failures |
| 0/1 Ready | Readiness probe failing | kubectl logs — app not returning 200 on /health |
| Connection refused | Service selector mismatch | Check labels on Pod match selector in Service |
When should I use Kubernetes vs just EC2 + Docker?
Kubernetes makes sense when: you have multiple services (microservices), you need automatic scaling, you want rolling deployments with zero downtime as a default, or your team already knows K8s. For a single service handling moderate traffic, EC2 + Docker Compose or a PaaS (Railway, Render) is simpler and cheaper. K8s has real operational overhead — make sure it solves a problem you actually have.
How much does a managed Kubernetes cluster cost?
GKE Autopilot charges only for running Pod resources — a 2-replica Node.js app using 200m CPU and 256Mi memory costs roughly $15–20/month. EKS charges $0.10/hour for the control plane (~₹600/month) plus EC2 node costs. For learning, GKE's $300 free credit or a local kind cluster costs nothing.
Do I need to learn Helm?
Helm is a package manager for Kubernetes — it templates manifests and manages releases. You do not need it to deploy your own app (plain kubectl apply works fine). Helm becomes valuable when: installing third-party software (databases, monitoring), managing multiple environments with different values, or packaging your app for distribution. Learn it after you are comfortable with raw manifests.
What is the difference between liveness and readiness probes?
Readiness: is this Pod ready to receive traffic? When it fails, the Pod is removed from Service endpoints but not restarted. Use it when your app needs a warm-up period or is temporarily overwhelmed. Liveness: is this Pod alive? When it fails a configured number of times, the Pod is restarted. Use it to recover from deadlocks or memory leaks. Both probes hitting /health is a common pattern.
Key Takeaways
What did you think?
Join 15,000+ Indian developers and creators receiving our curated newsletter every Sunday morning.
No spam. Only high-quality content. Unsubscribe anytime.