Kubernetes (K8s) has become the industry standard for running containerized applications at scale. You don't need to be a Kubernetes expert, but as a developer deploying applications, you need to understand enough to read configs, debug issues, and deploy your services correctly.
This guide covers the concepts that matter most for application developers — not cluster administration.
Before K8s, scaling a containerized application meant:
Kubernetes automates all of this. You declare what you want ("run 3 copies of this container"), and Kubernetes makes it happen and keeps it that way.
Kubernetes has one fundamental concept: desired state. You declare the state you want, and Kubernetes continuously works to make the actual state match it.
The smallest deployable unit. A Pod wraps one (usually) or more containers that share a network and storage:
# pod.yaml — you rarely create Pods directly
apiVersion: v1
kind: Pod
metadata:
name: api-pod
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/yourusername/myapi:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"In practice you never create Pods directly — you create Deployments that manage Pods for you.
A Deployment manages a set of identical Pods. It handles scaling, rolling updates, and rollbacks:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-deployment
labels:
app: api
spec:
replicas: 3 # Run 3 identical pods
selector:
matchLabels:
app: api
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Add 1 extra pod during update
maxUnavailable: 0 # Never take pods down before new ones are ready
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/yourusername/myapi:sha-abc123
ports:
- containerPort: 3000
envFrom:
- secretRef:
name: api-secrets
- configMapRef:
name: api-config
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "1000m"The readinessProbe tells K8s when a Pod is ready to receive traffic. The livenessProbe tells K8s when to restart a Pod. These are critical — without them, K8s might send traffic to a pod that's starting up or stuck.
Pods are ephemeral — they get new IP addresses when restarted. A Service provides a stable endpoint that routes to healthy Pods:
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api # Routes to pods with this label
ports:
- port: 80
targetPort: 3000
type: ClusterIP # Internal only — use LoadBalancer for external accessService types:
ClusterIP — accessible only within the cluster (for internal microservices)LoadBalancer — creates a cloud load balancer with a public IPNodePort — exposes on a port on each node (rarely used in production)Environment-specific configuration belongs in ConfigMaps and Secrets, not baked into images:
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
NODE_ENV: "production"
LOG_LEVEL: "info"
PORT: "3000"
---
# secret.yaml — base64 encode values: echo -n "value" | base64
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXM6Ly8... # base64 encoded
JWT_SECRET: c2VjcmV0... # base64 encodedIn production, use a secrets manager (AWS Secrets Manager, Vault) rather than storing secrets in YAML files.
An Ingress routes external HTTP traffic to the right Service based on host and path:
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- api.yourdomain.com
secretName: api-tls
rules:
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80# Deploy / apply configs
kubectl apply -f deployment.yaml
kubectl apply -f . # Apply all YAML files in current directory
# Check status
kubectl get pods
kubectl get deployments
kubectl get services
# Detailed info for debugging
kubectl describe pod api-deployment-abc123
kubectl describe deployment api-deployment
# View logs
kubectl logs api-deployment-abc123
kubectl logs -l app=api --tail=100 -f # Logs from all api pods
# Shell into a running pod
kubectl exec -it api-deployment-abc123 -- sh
# Rolling update (change image version)
kubectl set image deployment/api-deployment api=ghcr.io/yourusername/myapi:sha-newversion
# Check rollout status
kubectl rollout status deployment/api-deployment
# Rollback to previous version
kubectl rollout undo deployment/api-deployment
# Scale up/down
kubectl scale deployment api-deployment --replicas=51. Not setting resource requests and limits. Without resources, a single misbehaving pod can consume all cluster resources. Always set both.
2. Missing readiness probes. K8s will send traffic to a pod the moment it starts, before your app is ready. Add a readiness probe that checks your /health endpoint.
3. Using latest image tags. K8s caches images. If you push a new latest and the image is already cached, pods won't update. Always use specific SHA or version tags.
4. Putting secrets in ConfigMaps. ConfigMaps are not encrypted. Passwords, API keys, and connection strings belong in Secrets (or better, an external secrets manager).
5. No resource quotas per namespace. In multi-team clusters, set resource quotas to prevent one team's deployment from starving others.
latest, in Kubernetes manifestskubectl describe and kubectl logs resolve 90% of deployment issues