DevOps

Kubernetes Basics for Developers

Mayur Dabhi
Mayur Dabhi
May 18, 2026
15 min read

You've mastered Docker — containerizing apps, writing Dockerfiles, spinning up services with docker-compose. But what happens when you have dozens of containers spread across multiple servers that need to communicate, auto-scale, and recover from failures without manual intervention? That's exactly where Kubernetes steps in. Kubernetes (often abbreviated as K8s) is the de-facto standard for container orchestration, trusted by companies ranging from startups to Fortune 500 enterprises. In this guide, you'll learn the core concepts every developer needs to confidently work with Kubernetes: what it is, how it's architected, and how to deploy, expose, and scale your first application.

The Origin of Kubernetes

Kubernetes was born inside Google, inspired by an internal cluster management system called Borg that handled billions of container launches per week. Google open-sourced it in 2014 and donated it to the Cloud Native Computing Foundation (CNCF). Today it has over 100,000 GitHub stars and powers production workloads at Netflix, Airbnb, Spotify, and thousands of other companies.

What is Kubernetes and Why You Need It

Kubernetes is an open-source platform that automates the deployment, scaling, and management of containerized applications. Think of it as an operating system for your data center — you tell Kubernetes what you want (3 replicas of my API, always), and it figures out how to achieve and maintain that state across your fleet of servers.

Without container orchestration, teams face several painful problems as they scale:

Kubernetes solves all of these. Its core value proposition for developers can be summarized as:

Kubernetes Cluster Control Plane API Server etcd Scheduler Controller Manager kubectl (Developer) Worker Node 1 kubelet + kube-proxy Pod A C1 Pod B C2 Pod C (pending...) Worker Node 2 kubelet + kube-proxy Pod D C3 Pod E C4

Kubernetes Cluster Architecture: Control Plane manages Worker Nodes that run Pods containing your containers

Core Concepts: Clusters, Nodes, and Pods

Before writing a single YAML file, you need a mental model of Kubernetes' building blocks. These concepts nest inside each other like Russian dolls.

Cluster

A cluster is the top-level Kubernetes environment. It consists of a Control Plane (the "brain" that manages the cluster) and one or more Worker Nodes (the machines that run your actual application workloads). When you interact with Kubernetes via kubectl, you're talking to the cluster's API server.

Node

A node is a worker machine — a VM or a physical server. Every node runs three critical Kubernetes components:

Pod

The Pod is the smallest and most fundamental unit in Kubernetes. A pod wraps one or more tightly coupled containers that share the same network namespace (same IP address) and storage volumes. In practice, most pods run a single container. The key insight is that you never manage containers directly in Kubernetes — you manage pods.

my-pod.yaml — minimal Pod definition
apiVersion: v1
kind: Pod
metadata:
  name: my-api-pod
  labels:
    app: my-api
spec:
  containers:
  - name: my-api
    image: nginx:1.25
    ports:
    - containerPort: 80
    resources:
      requests:
        memory: "64Mi"
        cpu: "100m"
      limits:
        memory: "128Mi"
        cpu: "250m"
Don't Run Bare Pods in Production

While you can create a Pod directly, you almost never should in production. Bare pods are not rescheduled if the node they're on dies. Always use a Deployment (or StatefulSet for stateful workloads) which gives you self-healing and rolling updates automatically.

Setting Up Your Local Environment

You need two tools to work with Kubernetes: kubectl (the CLI that talks to your cluster) and a local cluster to practice on. Minikube is the easiest option for a single-node local cluster.

1

Install kubectl

The Kubernetes command-line tool. This is how you deploy applications, inspect resources, and manage your cluster.

Terminal — install kubectl
# Linux (amd64)
curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/

# macOS (Homebrew)
brew install kubectl

# Windows (Chocolatey)
choco install kubernetes-cli

# Verify installation
kubectl version --client
2

Install Minikube

A single-node Kubernetes cluster that runs on your local machine. Perfect for development and learning.

Terminal — install and start Minikube
# Linux
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

# macOS
brew install minikube

# Start a local Kubernetes cluster
minikube start

# Verify everything is working
kubectl cluster-info
kubectl get nodes
3

Explore the Kubernetes Dashboard

Minikube ships with the Kubernetes dashboard — a web UI to visualize your cluster state without memorizing kubectl commands.

Terminal — open dashboard
# Opens the dashboard in your default browser
minikube dashboard

# Check what's running in the cluster
kubectl get all --all-namespaces

Deployments: Declaring Your Desired State

A Deployment is the standard way to run a stateless application in Kubernetes. You declare your desired state (which image to run, how many replicas, resource limits) and Kubernetes' controller continuously works to make reality match that declaration. This is the reconciliation loop at the heart of Kubernetes.

When you create a Deployment, Kubernetes automatically creates a ReplicaSet (which manages the actual pods). You rarely interact with ReplicaSets directly — Deployments manage them for you.

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
  labels:
    app: my-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-api
    spec:
      containers:
      - name: my-api
        image: my-api:1.0.0
        ports:
        - containerPort: 3000
        resources:
          requests:
            memory: "128Mi"
            cpu: "250m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        env:
        - name: NODE_ENV
          value: "production"
        - name: PORT
          value: "3000"
Terminal — apply and inspect deployment
# Apply the deployment manifest
kubectl apply -f deployment.yaml

# Watch pods come up in real time
kubectl get pods -w

# Describe a specific pod (shows events, errors)
kubectl describe pod my-api-7d6f8b9c5-xk2p9

# Check deployment rollout status
kubectl rollout status deployment/my-api

# View deployment details
kubectl describe deployment my-api

# Stream logs from a pod
kubectl logs -f my-api-7d6f8b9c5-xk2p9

# Execute a command inside a running pod
kubectl exec -it my-api-7d6f8b9c5-xk2p9 -- /bin/sh

Rolling Updates and Rollbacks

One of Kubernetes' most valuable features is zero-downtime deployments. With the RollingUpdate strategy, Kubernetes brings up new pods before terminating old ones, ensuring your application stays available throughout a deployment.

Terminal — update image and rollback
# Deploy a new version by updating the image
kubectl set image deployment/my-api my-api=my-api:1.1.0

# Monitor the rollout
kubectl rollout status deployment/my-api

# View rollout history
kubectl rollout history deployment/my-api

# Rollback to previous version if something goes wrong
kubectl rollout undo deployment/my-api

# Rollback to a specific revision
kubectl rollout undo deployment/my-api --to-revision=2

Services: Stable Network Endpoints

Pods are ephemeral. They get new IP addresses every time they restart or are rescheduled to a different node. A Service gives you a stable virtual IP (ClusterIP) and DNS name that always routes traffic to the healthy pods behind it, regardless of which pods are currently running or what their IPs are.

Client / Ingress :80 Service my-api-service ClusterIP: 10.96.0.1 Pod (app=my-api) 10.244.0.5:3000 Pod (app=my-api) 10.244.1.3:3000 Pod (app=my-api) 10.244.0.9:3000 Service selects pods by label: app=my-api, load-balances traffic across all healthy replicas

A Kubernetes Service acts as a stable load-balanced endpoint in front of ephemeral Pods

Kubernetes offers four Service types for different networking scenarios:

Service Type Accessibility Typical Use Case
ClusterIP (default) Internal to cluster only Microservice-to-microservice communication
NodePort External via node IP + static port (30000–32767) Local development, quick testing
LoadBalancer External via cloud provider load balancer Production external traffic (AWS ELB, GCP LB)
ExternalName Maps service to external DNS name Accessing external databases or third-party APIs
service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-api-service
spec:
  # Selects pods with this label — must match Deployment's pod labels
  selector:
    app: my-api
  type: LoadBalancer
  ports:
  - name: http
    protocol: TCP
    port: 80           # Port the service listens on
    targetPort: 3000   # Port your container listens on
---
# For internal services between microservices, use ClusterIP:
apiVersion: v1
kind: Service
metadata:
  name: database-service
spec:
  selector:
    app: postgres
  type: ClusterIP
  ports:
  - port: 5432
    targetPort: 5432
Terminal — apply service and get URL
# Apply the service
kubectl apply -f service.yaml

# Check service status
kubectl get services

# On Minikube, get the service URL
minikube service my-api-service --url

# Forward a port for quick local testing (no service needed)
kubectl port-forward pod/my-api-7d6f8b9c5-xk2p9 8080:3000

ConfigMaps and Secrets

Hard-coding configuration values into your container images creates inflexible, insecure artifacts that can't be reused across environments. Kubernetes solves this with two primitives: ConfigMaps for non-sensitive configuration and Secrets for sensitive data.

Secrets Are Not Encrypted by Default

Kubernetes Secrets are base64-encoded, not encrypted. Anyone with access to the cluster's etcd can read them. For production, always enable encryption at rest for etcd and consider using a dedicated secrets manager like HashiCorp Vault, AWS Secrets Manager, or Sealed Secrets.

Store non-sensitive environment-specific configuration:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  # Key-value pairs become environment variables
  NODE_ENV: "production"
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  # Or mount entire config files
  app.json: |
    {
      "port": 3000,
      "timeout": 30,
      "retries": 3
    }

Create from the command line:

# From literal values
kubectl create configmap app-config \
  --from-literal=NODE_ENV=production \
  --from-literal=LOG_LEVEL=info

# From a file
kubectl create configmap app-config --from-file=app.json

# From a directory
kubectl create configmap app-config --from-file=./config/

Store sensitive data (base64-encoded in the manifest):

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  # Values MUST be base64-encoded
  # echo -n "mypassword" | base64
  DB_PASSWORD: bXlwYXNzd29yZA==
  API_KEY: c2VjcmV0YXBpa2V5MTIz
  JWT_SECRET: bXlzdXBlcnNlY3JldGp3dA==

Create secrets without checking values into git:

# Create directly from literal (kubectl handles base64)
kubectl create secret generic app-secrets \
  --from-literal=DB_PASSWORD=mypassword \
  --from-literal=API_KEY=secretapikey123

# From a .env file (keep outside of git)
kubectl create secret generic app-secrets \
  --from-env-file=.env.production

# View secret (base64-encoded)
kubectl get secret app-secrets -o yaml

# Decode a secret value
kubectl get secret app-secrets \
  -o jsonpath='{.data.DB_PASSWORD}' | base64 --decode

Inject config and secrets into your pod spec:

spec:
  containers:
  - name: my-api
    image: my-api:1.0.0
    # Load all keys from ConfigMap as env vars
    envFrom:
    - configMapRef:
        name: app-config
    - secretRef:
        name: app-secrets
    # Or load specific keys
    env:
    - name: DATABASE_URL
      valueFrom:
        secretKeyRef:
          name: app-secrets
          key: DB_PASSWORD
    # Mount ConfigMap as a volume (for config files)
    volumeMounts:
    - name: config-volume
      mountPath: /etc/config
  volumes:
  - name: config-volume
    configMap:
      name: app-config

Scaling and Health Checks

Kubernetes excels at scaling. You can scale manually with a single command or set up automatic scaling based on CPU or memory utilization. But before Kubernetes will route traffic to a pod, that pod needs to pass your health checks.

Manual and Automatic Scaling

Terminal — manual scaling
# Scale to 5 replicas
kubectl scale deployment my-api --replicas=5

# Scale back down
kubectl scale deployment my-api --replicas=2

# Watch pods scale in real time
kubectl get pods -w

For automatic scaling, use a HorizontalPodAutoscaler (HPA). It monitors CPU/memory utilization and adjusts the replica count to keep utilization near your target:

hpa.yaml — Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Liveness and Readiness Probes

Kubernetes uses two types of health checks to decide whether to route traffic to a pod and whether to restart it:

deployment-with-probes.yaml
spec:
  containers:
  - name: my-api
    image: my-api:1.0.0
    ports:
    - containerPort: 3000
    livenessProbe:
      httpGet:
        path: /healthz
        port: 3000
      initialDelaySeconds: 30   # Wait 30s after start before checking
      periodSeconds: 10          # Check every 10 seconds
      failureThreshold: 3        # Restart after 3 consecutive failures
      timeoutSeconds: 5
    readinessProbe:
      httpGet:
        path: /ready
        port: 3000
      initialDelaySeconds: 10   # Start checking sooner than liveness
      periodSeconds: 5
      failureThreshold: 3
      successThreshold: 1
Probe Endpoint Best Practices

Your /healthz (liveness) endpoint should only check that the process is alive — avoid checking external dependencies like databases. Your /ready (readiness) endpoint should check all dependencies: if the database connection pool is exhausted, return 503 so Kubernetes stops sending traffic until it recovers.

Essential kubectl Commands

You'll use these commands daily. Understanding them is more important than memorizing — use kubectl --help or kubectl explain <resource> liberally.

Command What It Does
kubectl get pods List all pods in current namespace
kubectl get pods -A List pods across all namespaces
kubectl describe pod <name> Detailed pod info including events and errors
kubectl logs <pod> -f Stream logs from a pod in real time
kubectl exec -it <pod> -- sh Open an interactive shell inside a pod
kubectl apply -f <file.yaml> Create or update resources from a YAML file
kubectl delete -f <file.yaml> Delete resources defined in a YAML file
kubectl get events --sort-by=.lastTimestamp View cluster events (great for debugging)
kubectl top nodes Show CPU and memory usage per node
kubectl top pods Show CPU and memory usage per pod
kubectl explain deployment.spec Documentation for any resource field in-CLI
kubectl config get-contexts List all configured clusters/contexts
kubectl config use-context <name> Switch between clusters (dev, staging, prod)

Namespaces: Logical Isolation Within a Cluster

Namespaces let you split a single cluster into isolated virtual clusters. They're used to separate teams, environments (dev/staging), or applications without provisioning separate clusters. Resources in different namespaces are isolated from each other by default.

# Create a namespace
kubectl create namespace my-team

# Deploy to a specific namespace
kubectl apply -f deployment.yaml -n my-team

# Set a default namespace for your session
kubectl config set-context --current --namespace=my-team

# Kubernetes ships with 4 built-in namespaces:
# default      — where resources go if you don't specify
# kube-system  — Kubernetes system components (don't touch)
# kube-public  — readable by all, used for cluster info
# kube-node-lease — node heartbeat objects

Putting It All Together

Here's a minimal but production-quality manifest that combines a Deployment, Service, ConfigMap, and HPA into a single file — a realistic starting point for deploying any stateless web service:

complete-app.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: production
data:
  NODE_ENV: "production"
  LOG_LEVEL: "warn"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-api
    spec:
      containers:
      - name: my-api
        image: my-api:1.0.0
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: app-config
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /healthz
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: my-api-service
  namespace: production
spec:
  selector:
    app: my-api
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 3000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-api-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Key Takeaways

  • Pods are the atomic unit — but always manage them through Deployments, not directly.
  • Deployments declare desired state and give you rolling updates, rollbacks, and self-healing for free.
  • Services provide stable network endpoints for your ephemeral pods — use ClusterIP internally, LoadBalancer externally.
  • ConfigMaps and Secrets decouple configuration from code — never bake env-specific values into images.
  • Health probes (readiness + liveness) are non-negotiable in production — without them, Kubernetes can't route traffic safely or recover from failures.
  • HPA automates scaling based on real metrics — set resource requests correctly or HPA won't work as expected.
  • Start with Minikube locally, then move to a managed service like EKS (AWS), GKE (GCP), or AKS (Azure) for production.
"Kubernetes doesn't just run containers — it encodes your operational knowledge into machine-executable policies. Define it once, enforce it everywhere."

Kubernetes has a steep initial learning curve, but once the mental model clicks — desired state, reconciliation loops, labels & selectors — everything else falls into place. The investment pays off exponentially as your applications grow in complexity and traffic. Start small: deploy a single app with a Deployment and Service on Minikube, add health checks, try scaling, trigger a rollback. By the time you've done that end-to-end, you'll understand 80% of what you need for day-to-day Kubernetes work.

Kubernetes Containers Orchestration DevOps kubectl K8s Docker
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.