NovuSpark
All articles
ContainerizationSeptember 5, 2025 · NovuSpark Team

Kubernetes 101: Pods, Deployments, and Services

This is the first post in our Kubernetes fundamentals series. Later posts cover networking, ConfigMaps and Secrets, storage, and autoscaling.

Docker (covered in our previous series) answers "how do I package and run one container." Kubernetes answers a different question: "how do I run hundreds of containers, across many machines, so that if one fails, something notices and fixes it automatically." That's a genuinely different problem, and it needs a few new concepts to solve it.

Pods: the smallest thing Kubernetes schedules

A Pod is the smallest deployable unit in Kubernetes — not a container itself, but a thin wrapper around one or more containers that always get scheduled onto the same machine and share a network namespace.

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  containers:
    - name: web
      image: my-app:1.0
      ports:
        - containerPort: 8080
kubectl apply -f pod.yaml
kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
web-pod   1/1     Running   0          12s

Most Pods run exactly one container — the multi-container case exists for genuinely coupled containers that must share a network and lifecycle, like a "sidecar" that ships logs from a main application container sitting right next to it in the same Pod.

Why you don't create Pods directly

Here's the detail that actually matters: delete that Pod, and Kubernetes does not recreate it.

kubectl delete pod web-pod

A bare Pod has no supervisor watching it. If the node it's running on fails, or someone deletes it, it's simply gone. That's almost never what you actually want in a real system — which is exactly why, in practice, you essentially never create bare Pods. You create a Deployment, and let it create and manage Pods for you.

DeploymentReplicaSetmaintains: 3Pod (running)Pod (running)Pod deleted →recreated in secondsa bare Pod (no Deployment above it) has none of this — deleted means gone
Fig. 1 — the Deployment → ReplicaSet → Pod chain is what makes Kubernetes self-healing

Deployments: the thing that actually keeps Pods running

# deployment.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: my-app:1.0
          ports:
            - containerPort: 8080
kubectl apply -f deployment.yaml
kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
web-7d9f8c6b5d-2xk9p   1/1     Running   0          8s
web-7d9f8c6b5d-4mn7q   1/1     Running   0          8s
web-7d9f8c6b5d-9wq2r   1/1     Running   0          8s

A Deployment declares "I want 3 replicas of this Pod template, always." It creates a ReplicaSet (a controller whose only job is maintaining a specific replica count) which in turn creates the actual Pods. Delete one of those Pods, and — unlike the bare Pod earlier — Kubernetes notices the replica count has dropped below 3 and creates a replacement within seconds, without anyone intervening:

kubectl delete pod web-7d9f8c6b5d-2xk9p
kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
web-7d9f8c6b5d-4mn7q   1/1     Running   0          45s
web-7d9f8c6b5d-9wq2r   1/1     Running   0          45s
web-7d9f8c6b5d-k8j3x   1/1     Running   0          3s

This self-healing behavior — actual state continuously reconciled against desired state — is the core idea underneath essentially everything in Kubernetes, and it's the direct payoff of never creating bare Pods in a real deployment. It's the same reconciliation-loop concept as Terraform continuously comparing state to configuration, covered earlier in this blog, just running continuously inside the cluster rather than on demand when you run apply.

Rolling updates: changing the image without downtime

kubectl set image deployment/web web=my-app:2.0
kubectl rollout status deployment/web
Waiting for deployment "web" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "web" successfully rolled out

A Deployment updates Pods gradually by default — creating new ones running the new image, waiting for them to become ready, then removing old ones — rather than terminating everything at once. If a rollout turns out to be bad:

kubectl rollout undo deployment/web

reverts to the previous version, the same gradual way. kubectl rollout history deployment/web shows every previous revision, and kubectl rollout undo deployment/web --to-revision=3 reverts to a specific one rather than just the immediately prior version — genuinely useful when a bad rollout wasn't caught immediately and more than one revision has shipped since.

Readiness and liveness probes: telling Kubernetes when a Pod is actually healthy

A container process being "running" doesn't mean the application inside it is ready for traffic — the same gap covered by Docker's HEALTHCHECK in our production Dockerfiles post. Kubernetes has two related but distinct probes:

      containers:
        - name: web
          image: my-app:1.0
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20

A readiness probe controls whether a Pod receives traffic from a Service at all — a Pod that fails readiness is removed from a Service's routing without being restarted, useful for a Pod that's temporarily busy (warming a cache, finishing a slow startup task) but not actually broken. A liveness probe controls whether Kubernetes restarts the container entirely — a Pod that fails liveness repeatedly gets killed and recreated, on the assumption that it's stuck in a state it can't recover from on its own. Conflating the two is a common mistake: a liveness probe that's too aggressive can cause Kubernetes to repeatedly kill and restart a Pod that's simply doing legitimately slow work, rather than one that's actually broken.

Services: giving a moving target a stable address

Pods are disposable — they get recreated, and each new one gets a new IP address. Nothing that depends on a Pod's IP directly can survive a Pod being replaced, which happens routinely. A Service solves this by providing one stable address that automatically routes to whichever Pods currently match its selector.

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080
kubectl apply -f service.yaml

Any other Pod in the cluster can now reach this Deployment's Pods at http://web, regardless of how many times individual Pods get replaced underneath it — the selector: app: web is what continuously matches the Service to whichever Pods currently carry that label, updating automatically as Pods come and go. We go much deeper on exactly how that routing works, and the different Service types available, in the next post in this series.

Init containers: setup work before the main container starts

A Pod occasionally needs preparatory work done before its main container should even start — waiting for a dependency to become reachable, running a database migration, fetching a configuration file. Init containers run to completion, in order, before any regular container in the Pod starts:

spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command: ["sh", "-c", "until nc -z db 5432; do sleep 2; done"]
  containers:
    - name: web
      image: my-app:1.0

This is a more explicit, Kubernetes-native version of the "don't assume a dependency is ready just because it started" caution flagged for Docker Compose's depends_on earlier in this blog — the web container's process genuinely doesn't start at all until wait-for-db has successfully exited, rather than starting immediately and hoping the database happens to be ready by the time it tries to connect.

Labels and selectors: the mechanism underneath everything in this post

Every connection covered in this post — a Deployment managing Pods, a Service routing to them — works purely through labels (arbitrary key-value pairs attached to objects) and selectors (a query matching against those labels), not through any direct reference by name:

metadata:
  labels:
    app: web
    tier: frontend
    environment: production

A Service's selector: app: web doesn't reference specific Pod names at all — it matches any Pod currently carrying that label, which is precisely what allows Pods to be freely replaced without the Service needing to be updated. This label-based, rather than name-based, coupling is worth understanding explicitly: it's the same loosely-coupled matching pattern that makes the whole system self-healing and flexible, rather than an incidental implementation detail.

What to actually remember from this post

  • A Pod is the smallest schedulable unit, but bare Pods have no self-healing — almost nothing in real usage creates them directly.
  • A Deployment manages Pods for you via an intermediate ReplicaSet, maintaining a desired replica count and replacing failed or deleted Pods automatically.
  • Rolling updates replace Pods gradually, with an easy, equally gradual rollback if something goes wrong — including to a specific prior revision, not just the immediately previous one.
  • Readiness and liveness probes answer different questions — "should this Pod receive traffic" versus "should this Pod be restarted" — and conflating them causes real, avoidable instability.
  • A Service provides a stable address for a set of Pods whose individual IPs are constantly changing underneath it.

Next in the series: Kubernetes Networking: ClusterIP, NodePort, and Ingress, where we cover how traffic actually gets from outside the cluster to a Service.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.