01-foundations-control-plane

Pods, Init Containers and Native Sidecars

The smallest schedulable unit, why you almost never create one directly, and the sidecar support that became stable in v1.33 after years of workarounds.

September 13, 2026
kubernetespodsinit-containerssidecarslifecyclerestartpolicy

Why the Unit Isn't a Container

Kubernetes doesn't schedule containers. It schedules pods — one or more containers that are always placed together on one node and share two things:

  • A network namespace. All containers in a pod share one IP address and can reach each other on localhost. They also share the port space, so two containers in one pod cannot both bind port 8080.
  • Optional shared volumes. Containers in a pod can mount the same volume and pass files between them.

If you've done the Docker roadmap, this is the network namespace from container fundamentals, extended to cover a small group of containers rather than one.

💡

Note the inversion: inside a pod, localhost does reach your sibling container — the opposite of the separate-containers case in Docker. Between pods it does not. That's the same namespace rule producing both behaviours.

The overwhelmingly common case is one application container per pod. Additional containers are for helpers that must share the network or filesystem — a log shipper, a proxy, a metrics exporter.

Why You Rarely Create Pods Directly

You can apply a bare Pod manifest. You almost never should.

A bare pod has nothing watching it. If its node fails, the pod is gone and nothing recreates it — no controller owns it, and from the previous guide, controllers are what make things persist.

Instead you declare a workload controller that manages pods for you:

ControllerUse for
DeploymentStateless applications — the default choice
StatefulSetWorkloads needing stable identity and per-replica storage
DaemonSetOne pod per node — log collectors, node agents
JobRun-to-completion work
CronJobScheduled work

Bare pods are for debugging and one-off experiments:

bash
kubectl run tmp --rm -it --image=busybox --restart=Never -- sh

Pod Phases and restartPolicy

A pod's status.phase is coarse — Pending, Running, Succeeded, Failed, Unknown. The useful detail is one level down, in container statuses and conditions.

The distinction that trips people up: Running is not Ready. A pod is Running when its containers have started. It is Ready only when its readiness probes pass, and only Ready pods receive Service traffic.

bash
kubectl get pods
# NAME                   READY   STATUS    RESTARTS   AGE
# api-7d4f8b9c5-x2kfp    0/1     Running   0          2m
#                        ^^^ Running but NOT ready — receiving no traffic

restartPolicy governs what happens when a container exits:

  • Always (default, and the only valid value for Deployments) — restart regardless of exit code.
  • OnFailure — restart only on a non-zero exit. For Jobs.
  • Never — don't restart. For Jobs where a controller handles retries.

Repeated restarts trigger exponential backoff, which is what CrashLoopBackOff is: not an error itself, but the waiting state between restart attempts.

⚠️

CrashLoopBackOff describes the loop, never the cause. The cause is in the previous container's logs, because the current attempt may not have produced any yet:

kubectl logs <pod> --previous

Together with the Last State block in kubectl describe pod, that's where the actual error lives.

Init Containers

Init containers run to completion, in order, before any app container starts. If one fails, the pod restarts it (subject to restartPolicy) and the app containers never begin.

yaml
spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command: ['sh', '-c', 'until nc -z db 5432; do sleep 2; done']
    - name: run-migrations
      image: myapp:1.4.0
      command: ['npm', 'run', 'migrate']
  containers:
    - name: api
      image: myapp:1.4.0

Good uses: schema migrations, waiting on a dependency, fetching config or secrets, setting file permissions on a volume.

Because they're separate containers, they can use a different image — so a tool needed only at startup doesn't have to be baked into your application image. That's the same build-versus-runtime separation as a multi-stage Docker build, applied at the pod level.

Native Sidecar Containers

Here's the genuinely modern part, and it fixed real, long-standing pain.

A sidecar is a helper that runs alongside your application for the pod's whole life — a log shipper, a service-mesh proxy, a metrics agent. For years people implemented these as ordinary extra entries in containers, which caused two well-known problems:

  1. No startup ordering. Containers in containers start in parallel, so your application could begin before the mesh proxy was ready and fail its first requests.
  2. Jobs that never finished. A Job completes when its containers exit. A sidecar that runs forever means the Job runs forever.

Kubernetes solved this with native sidecar containers, which reached stable in v1.33. A sidecar is declared as an init container with restartPolicy: Always:

yaml
spec:
  initContainers:
    - name: log-shipper
      image: fluent-bit:3.1
      restartPolicy: Always      # <- this makes it a sidecar
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  containers:
    - name: api
      image: myapp:1.4.0
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  volumes:
    - name: logs
      emptyDir: {}

Declaring it in initContainers with restartPolicy: Always gives you three guarantees:

  • It starts before the app containers and is running when they begin.
  • It keeps running alongside them rather than exiting.
  • It doesn't block Job completion — it's terminated once the main containers finish.
💡

Always is currently the only valid restartPolicy value for an init container. The field isn't a general-purpose knob — it's specifically the marker that turns an init container into a sidecar.

Check yourself

A Job runs a data export with a logging agent declared as a second entry under `containers`. The export finishes in two minutes, but the Job never reports completion. Why?

Reading a Pod

Three commands cover most of what you need:

bash
kubectl get pod api-7d4f8b9c5-x2kfp -o wide     # node, IP, ready count
kubectl describe pod api-7d4f8b9c5-x2kfp        # events, container states, probe results
kubectl logs api-7d4f8b9c5-x2kfp -c api         # -c selects a container in a multi-container pod
kubectl logs api-7d4f8b9c5-x2kfp --previous     # the crashed attempt

In describe, two blocks repay careful reading:

  • State / Last State — including Reason: OOMKilled and Exit Code: 137, which is the memory limit from the resources guide in phase 4.
  • Events — the scheduler, kubelet and controllers all record here. This is where "why is this Pending?" is answered.

Check yourself

Which statement about containers within a single pod is correct?

What's Next

You know what a pod is and how it's built. Phase 2 covers what actually runs them in production — Deployments and rolling updates, the three probes that decide whether traffic reaches you, and the ConfigMap behaviour that makes config changes appear to do nothing.