All lessons
DevOps & cloudAdvanced15 min readWorth knowing

Kubernetes

An orchestrator that keeps many containers running, healthy and reachable across many machines.

  • Pods
  • Deployments
  • Services
  • Ingress
  • Helm
Step 1/4
Kubernetes is the captain. You tell it what you want — “3 copies of my app” — and it makes it so.

In 30 seconds

Kubernetes runs containers across a cluster of machines and keeps them in the state you describe: how many copies, which version, how much memory. If a container crashes or a machine dies, it starts replacements elsewhere. It also gives you load balancing, rolling updates and autoscaling, at the price of real complexity.

Not in my daily stack — I know the concepts and would learn the details when a project needs it.

Why it matters

  • Kubernetes matters because it became the standard platform for running containers at scale; every major cloud offers it as a managed service.
  • A team needs it when it runs many services across many machines and wants self-healing, autoscaling and safe rolling updates handled by the platform.
  • Its declarative model, where you describe the desired state in YAML and the cluster makes it so, fits well with Git-based workflows and review.
  • In my own projects, Docker Compose on a well-run server covers the needs at a fraction of the operational cost; Kubernetes is what I would reach for when a system needs multiple nodes and automatic failover.

How it works

01What problems Kubernetes solves

With a few containers on one server, Docker Compose is enough. With dozens of services on twenty machines, new questions appear. Which machine has room for this container (scheduling)? Who restarts it when it crashes, or moves it when the machine dies (self-healing)? How do we add copies when traffic grows (scaling)? How do we release a new version without downtime and roll back if it fails (rolling updates)? How do services find each other when their addresses keep changing (service discovery)? Kubernetes answers all of these with one idea: you declare the desired state, for example three replicas of web version 1.4.2, and controllers continuously compare the actual state with it and act to close the gap. This reconciliation loop is the heart of Kubernetes.

  • Control plane: API server, scheduler, controllers, etcd (the cluster's database).
  • Worker nodes: kubelet runs pods; a container runtime runs containers.
  • Everything is an API object you create, read and update with kubectl or GitOps tools.

02Pods and Deployments

A Pod is the smallest unit Kubernetes runs: one or more containers that share a network address and can share volumes. Pods are disposable; they get new names and IPs when replaced, so you almost never create them directly. A Deployment manages pods for you: it keeps the requested number of replicas running and performs rolling updates when you change the image. Two health checks matter. A readiness probe tells Kubernetes when a pod can receive traffic; a liveness probe tells it when a pod is stuck and must be restarted. Resource requests tell the scheduler how much CPU and memory to reserve; limits cap usage. Without requests, scheduling and autoscaling make poor decisions.

yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: web }
spec:
  replicas: 3
  selector: { matchLabels: { app: web } }
  template:
    metadata: { labels: { app: web } }
    spec:
      containers:
        - name: web
          image: registry.example.com/web:1.4.2
          ports: [{ containerPort: 3000 }]
          readinessProbe: { httpGet: { path: /healthz, port: 3000 } }
          resources:
            requests: { cpu: 250m, memory: 256Mi }
            limits: { memory: 512Mi }

03Services, configuration and autoscaling

A Service gives a stable name and virtual IP to a changing set of pods, selected by labels, and load-balances across the ready ones. Other pods reach it by DNS, for example http://web. ClusterIP services are internal only; LoadBalancer services ask the cloud for an external load balancer. Configuration is kept out of images: a ConfigMap holds non-secret settings, a Secret holds passwords and keys. Note that Secrets are only base64-encoded by default, so enable encryption at rest and restrict who can read them, or sync them from an external secret manager. A HorizontalPodAutoscaler (HPA) adjusts the replica count based on CPU, memory or custom metrics, within the minimum and maximum you set.

yaml
apiVersion: v1
kind: Service
metadata: { name: web }
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: 3000 }]
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: web }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }

04How a request reaches a pod

Follow one request. The browser resolves app.example.com through DNS to the address of a cloud load balancer. The load balancer forwards to the cluster's edge proxy, which is either an Ingress controller or, increasingly, a Gateway API implementation. That proxy reads routing rules (host and path) and sends the request to the matching Service. The Service, implemented on each node by kube-proxy or an eBPF data plane such as Cilium, picks one ready pod from its endpoints and forwards the request to the pod's IP and port. Only pods passing their readiness probe are in that list, which is how rolling updates avoid sending traffic to containers still starting. Note that the popular community ingress-nginx controller was retired in March 2026; new clusters should use Gateway API or a maintained controller.

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: web }
spec:
  parentRefs: [{ name: public-gateway }]
  hostnames: ["app.example.com"]
  rules:
    - matches: [{ path: { type: PathPrefix, value: / } }]
      backendRefs: [{ name: web, port: 80 }]

05Helm and managed Kubernetes

Real applications need many YAML files that differ slightly between staging and production. Helm packages them into a chart: templates plus a values file, so you install or upgrade a whole application with one command and can roll back to a previous release. Many third-party tools (databases, monitoring, cert-manager) ship as Helm charts. Kustomize is a template-free alternative built into kubectl, using overlays per environment. Running the control plane yourself is hard, so most teams use managed Kubernetes: Amazon EKS, Google GKE, Azure AKS or DigitalOcean DOKS. The provider runs and upgrades the control plane; you still manage node pools, upgrades of your workloads, networking add-ons, security policies and cost. Expect to upgrade the Kubernetes version regularly, since each minor release is supported for roughly a year upstream.

bash
helm install web ./charts/web -f values.prod.yaml -n prod --create-namespace
helm upgrade web ./charts/web -f values.prod.yaml -n prod --atomic --wait
helm history web -n prod
helm rollback web 3 -n prod
kubectl rollout status deployment/web -n prod

06When Kubernetes is overkill, and what to use instead

Kubernetes brings a lot: networking plugins, ingress or gateways, certificates, secrets, RBAC, monitoring, upgrades, and YAML for everything. For a team of a few developers running one product, that platform work can cost more than the product's actual infrastructure. If your app fits on one or two servers, Docker Compose with good backups and monitoring is simpler, cheaper and easier to debug. If you want containers with less operational work, managed container services such as AWS ECS with Fargate, Google Cloud Run or Fly.io handle scheduling and scaling without a cluster to manage. Tools like Kamal deploy containers to plain servers with zero-downtime switching. Choose Kubernetes when you have many services, real scaling needs and people who can own the platform.

  • One or two servers: Docker Compose.
  • Containers without cluster ops: ECS/Fargate, Cloud Run, Fly.io.
  • Simple multi-server deploys: Kamal, Docker Swarm, Nomad.

Use it when

  • Many services owned by several teams that need a common deployment platform.
  • Workloads that must survive node failures and scale automatically with traffic.
  • Organisations that want portable infrastructure across clouds or on-premises.
  • When the team has, or will hire, people to own the cluster.

Skip it when

  • A single monolith with predictable traffic that fits on one or two servers.
  • A small team without platform experience; the cluster becomes the product.
  • Stateful databases as a first step; run them managed or on dedicated servers until you are experienced.

Alternatives

  • Docker Compose on a VPS

    One file, one server, easy to understand; no automatic multi-node failover.

  • AWS ECS / Fargate

    AWS-native container orchestration with far fewer concepts than Kubernetes.

  • Google Cloud Run

    Run containers per request, scale to zero, no cluster to manage.

  • Kamal / Nomad / Docker Swarm

    Lighter ways to run containers on several servers.

Best practices

  1. Set resource requests and memory limits on every container.
  2. Use readiness and liveness probes correctly; readiness gates traffic, liveness restarts.
  3. Keep manifests in Git and deploy through CI or GitOps (Argo CD, Flux), not by hand.
  4. Use namespaces, RBAC and network policies to limit what each workload can reach.
  5. Pin image tags or digests; never deploy latest.
  6. Plan regular cluster upgrades; unsupported versions stop getting security fixes.

Common mistakes

  • Treating Secrets as encrypted when they are only base64 by default.
  • No resource requests, so pods get packed onto one node and evicted under pressure.
  • A liveness probe that checks the database, so a database blip restarts every pod at once.
  • Adopting Kubernetes for one small app and spending more time on the cluster than on the product.
  • Running kubectl apply from laptops, so nobody knows what is actually deployed.

Deep dive

07Inside the control plane: API server, etcd and watches

Every component in Kubernetes talks only to the API server; nothing talks to etcd or to each other directly. The API server authenticates the request, checks RBAC, runs admission controllers (mutating ones can add defaults or sidecars, validating ones can reject objects that break policy), and stores the object in etcd, a consistent key-value store that uses the Raft protocol. etcd needs a majority of members to accept writes, which is why clusters run three or five members: three tolerate one failure, five tolerate two. Controllers do not poll; they open long-lived watches and receive a stream of changes, keep a local cache, and reconcile. The scheduler watches for pods without a node, filters nodes that cannot fit them, scores the rest and writes its choice back. The kubelet on that node watches for pods assigned to it and starts their containers through the container runtime.

  • Admission: mutating webhooks, then validating webhooks and policies.
  • etcd quorum: 3 members tolerate 1 failure, 5 tolerate 2.
  • Back up etcd, or rely on the managed provider to do it.

08Requests, limits, QoS classes and CPU throttling

Requests are what the scheduler reserves; limits are what the kernel enforces. Together they place each pod in a quality-of-service class. Guaranteed: every container has requests equal to limits for CPU and memory. Burstable: some requests or limits are set. BestEffort: nothing set. When a node runs short of memory, the kubelet evicts BestEffort pods first, then Burstable pods that exceed their requests. Memory over the limit is not negotiable: the container is OOM-killed, shows exit code 137 and reason OOMKilled, and restarts. CPU over the limit is throttled instead, through the kernel's CFS quota, and throttling can add large latency spikes to a busy service even when the node has idle CPU. That is why many teams set a memory limit equal to the memory request, set CPU requests carefully, and leave CPU limits off for latency-sensitive services, while namespace quotas keep the total in check.

yaml
resources:
  requests: { cpu: 500m, memory: 512Mi }
  limits:   { memory: 512Mi }       # no CPU limit: avoid throttling
 
# diagnose
# kubectl describe pod web-7d9f -n prod | grep -A3 "Last State"
# kubectl top pod -n prod --containers

09Graceful shutdown and voluntary disruptions

When a pod is deleted, during a rollout, a scale-down or a node drain, two things start at the same time: the kubelet sends SIGTERM to the containers, and the endpoint controller removes the pod from Service endpoints. Proxies and load balancers learn about the removal a moment later, so for a second or two traffic can still arrive at a process that is already shutting down. A short preStop sleep delays SIGTERM until routing has caught up. The app must then stop accepting new work, finish in-flight requests and exit within terminationGracePeriodSeconds, 30 by default, after which it is killed. For planned events like node upgrades, a PodDisruptionBudget tells Kubernetes how many replicas may be down at once, and drains respect it. Topology spread constraints keep replicas on different nodes and zones, so one failure never takes all of them.

yaml
spec:
  terminationGracePeriodSeconds: 45
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: ScheduleAnyway
      labelSelector: { matchLabels: { app: web } }
  containers:
    - name: web
      lifecycle:
        preStop: { exec: { command: ["sleep", "5"] } }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: web }
spec: { minAvailable: 2, selector: { matchLabels: { app: web } } }

10A security baseline: Pod Security, RBAC and NetworkPolicy

By default a Kubernetes cluster is permissive: any pod can talk to any other pod, and a pod can run as root with broad privileges. Three built-in mechanisms tighten it. Pod Security Admission enforces the Pod Security Standards per namespace through labels: privileged, baseline or restricted. Restricted requires non-root, no privilege escalation, dropped capabilities and a seccomp profile, and it is a good target for application namespaces. RBAC should follow least privilege: namespaced Roles rather than ClusterRoles where possible, no wildcard verbs, and disable automatic service account token mounting for pods that never call the API. NetworkPolicy is a firewall for pods: start each namespace with a default deny, then allow only the flows you need, such as ingress from the gateway and egress to the database and DNS. NetworkPolicies are enforced only if the CNI plugin supports them, as Calico and Cilium do.

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: prod
  labels:
    pod-security.kubernetes.io/enforce: restricted
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: prod }
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]

Real-world scenarios

01

Pods are in CrashLoopBackOff after a release

Problem

A new image was rolled out and the new pods keep restarting with increasing back-off delays. The old ReplicaSet still serves traffic, but the rollout is stuck and the team is unsure where to look.

How to solve it

Start with kubectl describe pod to read the last state, exit code and events: 137 with OOMKilled points to memory, 1 to an application error, and probe failures show up as events. Then run kubectl logs <pod> --previous to see the output of the crashed container, not the current attempt. Common causes are a missing environment variable or Secret key, a failing migration on start-up, or a liveness probe that fires before the app is ready, fixed with a startupProbe. Because readiness kept old pods serving, there is no outage: run kubectl rollout undo deployment/web if a fix will take time, and add the missing check to CI.

02

New pods stay Pending

Problem

After scaling a Deployment from 3 to 10 replicas for a campaign, seven pods sit in Pending state for many minutes and the extra capacity never arrives.

How to solve it

Run kubectl describe pod on one of them and read the scheduler's event; it states exactly why, for example insufficient CPU on all nodes, an unmatched node selector or taint, or a volume bound to another zone. For capacity problems, check that the cluster autoscaler or Karpenter is installed and that the node pool maximum allows growth, and compare requests with real usage, because inflated requests waste capacity. kubectl describe node shows allocated versus allocatable resources. For zone-bound volumes, use a storage class with volumeBindingMode WaitForFirstConsumer. Before the next campaign, pre-scale or raise minReplicas ahead of time rather than relying on autoscaling under pressure.

03

Node upgrades cause short outages

Problem

Every monthly node pool upgrade causes a few minutes of errors. The Deployment has three replicas, yet users still see failures while nodes are drained.

How to solve it

Look at where the replicas run: often all three landed on one or two nodes, so draining one node removes most of the capacity. Add topology spread constraints across nodes and zones. Add a PodDisruptionBudget with minAvailable: 2 so a drain waits instead of evicting too many at once. Then check shutdown: a preStop sleep of a few seconds, SIGTERM handling that drains connections, and a grace period longer than the slowest request. Verify the readiness probe really reflects ability to serve. Finally test it: drain a node in staging with kubectl drain under synthetic load and confirm the error rate stays flat.

Cheat sheet

Recent events, newest last
kubectl get events -n prod --sort-by=.metadata.creationTimestamp
Why is this pod unhappy?
kubectl describe pod <pod> -n prod
Logs of the crashed attempt
kubectl logs <pod> -n prod --previous
Live resource usage
kubectl top pod -n prod --containers
Roll back a Deployment
kubectl rollout undo deployment/web -n prod
Debug a distroless pod with a temporary shell
kubectl debug -it <pod> -n prod --image=busybox:1.37 --target=web
Reach a ClusterIP service from your laptop
kubectl port-forward svc/web 8080:80 -n prod
Check what an identity may do
kubectl auth can-i list secrets -n prod --as=system:serviceaccount:prod:web
Drain a node for maintenance
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
Restart pods to pick up new config
kubectl rollout restart deployment/web -n prod

Questions & answers

Q1What is the difference between a Pod and a Deployment?

A Pod is the smallest deployable unit: one or more containers that share a network namespace and storage and are scheduled together on one node. Pods are ephemeral; if one dies or its node fails, it is not resurrected, and a replacement gets a new name and IP. A Deployment is a controller that manages a ReplicaSet, which in turn keeps a specified number of identical pods running. It also handles rolling updates and rollbacks when you change the pod template, such as the image tag. In practice you create Deployments, or StatefulSets for stateful workloads, and almost never bare Pods.

Q2What Service types exist and when do you use each?

ClusterIP is the default: a stable internal virtual IP and DNS name, reachable only inside the cluster, used for service-to-service traffic. NodePort opens the same port on every node, mainly useful for testing or for feeding an external load balancer. LoadBalancer asks the cloud provider to create an external load balancer pointing at the service, which is simple but costs one load balancer per service. ExternalName maps a service name to an external DNS name. For HTTP traffic, the usual pattern is ClusterIP services behind a single Gateway or Ingress, which does host and path routing and TLS for many services through one load balancer.

Q3How does a rolling update work, and what is the role of readiness probes?

When you change a Deployment's pod template, it creates a new ReplicaSet and gradually scales it up while scaling the old one down. maxSurge controls how many extra pods may exist during the update, and maxUnavailable how many may be missing. A new pod only counts as available once its readiness probe passes, and only ready pods receive traffic from Services. So if the new version fails to start or its readiness check fails, the rollout stalls instead of replacing healthy pods, and you can run kubectl rollout undo to return to the previous ReplicaSet. Without proper readiness probes, traffic hits pods that are not ready yet.

Q4What happens when a node dies?

The kubelet on that node stops reporting to the control plane, and after a timeout the node is marked NotReady. Pods on it become unreachable, endpoints are removed from Services so traffic stops going there, and after an eviction delay those pods are marked for deletion. Controllers such as Deployments notice they have fewer replicas than desired and create new pods, which the scheduler places on healthy nodes with enough capacity. Stateless apps recover automatically. Stateful workloads need more care: volumes tied to one zone must be reattached, and StatefulSets wait to be sure the old pod is gone before starting a replacement.

Q5ConfigMap versus Secret: how do you handle configuration and secrets?

Both inject configuration into pods as environment variables or mounted files. ConfigMaps hold non-sensitive settings such as feature flags or URLs. Secrets are meant for credentials, but by default they are only base64-encoded and stored in etcd, so anyone with read access to them sees the values. I enable encryption at rest for etcd, restrict Secret access with RBAC, avoid committing them to Git, and preferably sync them from an external manager such as AWS Secrets Manager or Vault through the External Secrets Operator, or use sealed or SOPS-encrypted secrets for GitOps. Pods typically need a restart or a reload mechanism to pick up changed values.

Q6When would you not use Kubernetes?

When the operational cost exceeds the benefit. A single application with steady traffic that fits on one or two servers runs perfectly well with Docker Compose, good backups and monitoring, and the whole team can understand and debug it. Kubernetes adds networking, gateways, certificates, RBAC, upgrades and a large YAML surface, and someone must own all of that. If you want containers without managing a cluster, ECS with Fargate or Cloud Run give autoscaling and rolling deploys with far fewer concepts. I would pick Kubernetes for many services, multiple teams, real autoscaling or portability needs, and people ready to run the platform.

Senior-level questions

S1How do you run several teams or customers on Kubernetes: namespaces or separate clusters?

Namespaces give soft multi-tenancy: each team gets its own namespace with RBAC, resource quotas, limit ranges, network policies and Pod Security labels. That is efficient and enough when tenants trust each other, such as teams in one company. But all tenants share the control plane, the nodes and the kernel, so a noisy neighbour or a container escape can affect others, and cluster-wide objects like CRDs are shared. Separate clusters give hard isolation, independent upgrades and clear cost attribution, at the price of more control planes to run and less efficient packing. A common middle path is a few clusters split by environment and sensitivity, for example production and non-production, with namespaces per team inside, plus dedicated node pools or clusters for untrusted or regulated workloads.

S2Should you run databases inside Kubernetes?

It is possible and increasingly common, but it should be a deliberate choice. StatefulSets give stable identities and persistent volumes, and operators such as CloudNativePG for PostgreSQL automate replication, failover, backups and upgrades, which is far better than hand-written manifests. The arguments for it: one platform, portability, and lower cost than managed services at scale. The arguments against: storage performance and volume behaviour depend on the CSI driver and the cloud, node drains and upgrades now affect your primary, and the team needs real database and Kubernetes expertise when something goes wrong at night. For most teams, a managed database such as RDS or Cloud SQL with Kubernetes running the stateless tiers is the safer default, and in-cluster databases fit platform teams that have proved their backup and failover drills.

S3What does GitOps add, and what are its trade-offs?

In GitOps, a controller inside the cluster, such as Argo CD or Flux, continuously compares the cluster with manifests in a Git repository and applies the difference. Git becomes the single source of truth: every change is a reviewed commit, history is the audit log, rollback is a revert, and drift caused by manual kubectl edits is detected and undone. CI no longer needs cluster credentials, because the cluster pulls changes, which shrinks the attack surface. The trade-offs: another system to run and secure; secrets need a separate solution such as External Secrets or SOPS; image tag bumps need automation that commits back to Git; and the repository structure for environments and clusters becomes a design problem of its own. For more than a couple of services or clusters, the benefits usually win.

S4How do you upgrade a Kubernetes cluster safely?

Upstream supports each minor version for about fourteen months and only the three most recent minors get patches, so upgrades are routine, one minor version at a time. Before upgrading, read the release notes for removed APIs and scan manifests and Helm charts with a tool like Pluto or kubent, because removed API versions break deployments. Check that add-ons such as the CNI, CSI drivers, ingress or gateway controller, and cert-manager support the target version. Upgrade staging first and run it for a while. In production, upgrade the control plane, then node pools with surge upgrades or by creating a new pool and draining the old one, which relies on PodDisruptionBudgets, topology spread and graceful shutdown being correct. Blue-green clusters, building a new cluster and shifting traffic, are the safest and most expensive option.

Glossary

Control plane
The components that store and act on cluster state: API server, etcd, scheduler and controller manager.
etcd
The consistent key-value store holding all cluster objects; it needs a majority of members to accept writes.
kubelet
The agent on every node that starts the pods assigned to it and reports their status.
Controller
A loop that watches objects and acts to move the actual state towards the desired state.
ReplicaSet
Keeps a fixed number of identical pods running; Deployments create and manage ReplicaSets for you.
StatefulSet
Runs pods with stable names, ordered start-up and their own persistent volumes, for databases and similar.
DaemonSet
Runs one copy of a pod on every node, typically for log collectors, monitoring agents or networking.
PodDisruptionBudget
A rule limiting how many pods of an app may be down at once during voluntary disruptions like drains.
QoS class
Guaranteed, Burstable or BestEffort, derived from requests and limits; decides eviction order under memory pressure.
CNI
Container Network Interface: the plugin, such as Cilium or Calico, that gives pods networking and enforces policies.

Have a product in mind?Let's build it.

Tell me about your idea, your product or your team. I usually reply within one working day.