Pods

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

A Pod is the smallest deployable unit in Kubernetes, and every workload controller covered later in this section — Deployments, StatefulSets, DaemonSets, Jobs — exists only to create, watch and replace Pods. This page is written against Kubernetes 1.37 (kubectl 1.37) and opens Guides & References → Backend Development → Kubernetes's workloads group: Health Probes and Container Lifecycle, ReplicaSets and Deployments and Deployment Strategies all assume the vocabulary introduced here. Everything below builds on the object model from Objects and the Kubernetes API: a Pod is simply an object whose kind is Pod.

The Pod as the unit of scheduling

Kubernetes never schedules a bare container. It schedules a Pod — one or more containers that are always placed on the same node, started (and stopped) together, and that share several kernel-level resources for as long as the Pod exists:

  • A network namespace. Every container in a Pod shares one network namespace, so they share one IP address and one port space, and can reach each other over localhost exactly as two processes on the same machine would. Two containers in the same Pod cannot both bind port 8080; two Pods can, because each Pod gets its own network namespace and its own IP. This is also why a Pod, not a container, is what a Service’s endpoint actually points at (see Services).

  • Shared volumes. A volumes entry declared at the Pod level can be mounted by more than one container in that Pod, each at its own path — the mechanism every multi-container pattern below depends on (see Volumes and Ephemeral Storage for the volume types themselves). Two containers in a Pod can therefore exchange files or a Unix socket without a network hop.

  • Shared fate. A Pod’s containers are scheduled together, start together and are torn down together. If the node running a Pod disappears, every container in it disappears with it; there is no "restart only the failed container on another node" — the replacement, if there is one, is a brand-new Pod, created by a controller.

  • IPC and UTS namespaces are shared the same way as the network namespace by default, so containers in a Pod can also communicate via System V IPC or POSIX message queues, and see the same hostname.

This bundle is often summarised as a "logical host": from inside any container in a Pod, the rest of the Pod looks like other processes on the same machine, reachable over localhost and the local filesystem, in the same way the sampler’s Book 1 introduces Pods as co-scheduled containers with a shared IP and shared fate. The practical rule that falls out of it is one (logically) independent process per container: a container should own one job, so the kubelet’s container-level restart, resource limits and logs stay meaningful; a Pod groups several such single-purpose containers only when they genuinely need that shared network and filesystem, not as a packaging convenience for otherwise-unrelated processes.

Why bare Pods are not durable

A Pod created directly (kubectl run with no controller, or a bare kind: Pod manifest apply`d) is, deliberately, not durable: if its node fails, if it is evicted for resource pressure, or if a human deletes it, nothing recreates it. There is no live migration of a running Pod to another node — a Pod that must move is deleted on the old node and a new one (a new name, a new IP, a new UID) is created on a new one. This is exactly the "Pods are fragile, short-lived" starting point Book 1 ch. 2 spends most of its space on, and it is also why the same book immediately reaches for externalising state: rather than trusting a Pod’s own filesystem to survive, an application keeps anything that must outlive a single Pod in a store built for that — Redis or Memcached for a cache, Cassandra (or, in this section’s Bookshelf scenario, PostgreSQL and Redis) for data that must survive a Pod restart. In current Kubernetes that same idea is formalised as a spectrum: `emptyDir survives container restarts but not Pod deletion, a PersistentVolumeClaim survives Pod deletion but is still just as replaceable as the Pod that mounts it, and a StatefulSet gives a replacement Pod the same stable identity and the same volume, rather than making it durable in place (see Volumes and Ephemeral Storage, Persistent Volumes and Storage Classes and StatefulSets). In every case the answer to "what happens when this Pod dies" is: a controller creates a different Pod, not that the same Pod is repaired or moved. Almost every Bookshelf manifest in this section is therefore wrapped in a controller (a Deployment, a StatefulSet, a Job) — see ReplicaSets and Deployments for how a Deployment supplies exactly that layer over a bare Pod template. Source: Kubernetes Docs — Pods.

apiVersion: v1
kind: Pod
metadata:
  name: catalog-api-debug
  namespace: bookshelf
  labels:
    app.kubernetes.io/name: catalog-api
    app.kubernetes.io/component: api
spec:
  containers:
    - name: catalog-api
      image: ghcr.io/example/bookshelf-catalog-api:1.0.0
      ports:
        - containerPort: 8080

Source: Kubernetes Docs — Pods. A Pod this bare is useful only for a one-off debugging session (kubectl run catalog-api-debug --image=…​ --restart=Never); every long-running Bookshelf component is created through a Deployment or StatefulSet instead.

Lifecycle: phases, conditions, container states and restartPolicy

Pod phases

status.phase is a coarse, one-word summary of where a Pod is in its life, and only ever moves forward:

Phase Meaning

Pending

Accepted by the API server, but not every container is set up and ready yet — this includes the time spent waiting to be scheduled and the time spent pulling images.

Running

Bound to a node, all containers created, and at least one is running, starting or restarting.

Succeeded

Every container terminated in success (exit code 0) and none will be restarted.

Failed

Every container terminated, and at least one terminated in failure.

Unknown

The Pod’s state could not be obtained, typically because the API server lost contact with the Pod’s node.

Pending and Running cover every long-lived Pod; Succeeded and Failed are terminal and are what Jobs and CronJobs actually watches for. CrashLoopBackOff and Terminating, which kubectl get pods prints in its STATUS column, are not phases — they are a kubectl display convenience layered on top of a container state and a deletion timestamp, respectively; the phase underneath a CrashLoopBackOff-displaying Pod is still Running (or Pending, before the first successful start). Source: Kubernetes Docs — Pod Lifecycle.

Pod conditions

status.conditions is a finer-grained array the kubelet and scheduler maintain alongside the phase, each entry with a type, a status (True/False/Unknown), lastTransitionTime and a machine-readable reason:

Condition Set once…​

PodScheduled

the Pod has been bound to a node.

PodReadyToStartContainers

the Pod’s sandbox exists: network namespace created, volumes mounted, any requested dynamic resources allocated.

Initialized

every init container (below) has completed successfully.

ContainersReady

every container in the Pod reports ready (see Health Probes and Container Lifecycle).

Ready

the Pod as a whole should receive traffic from matching Services — ContainersReady plus any readiness gate.

DisruptionTarget

the Pod is about to be terminated by a disruption (preemption, eviction or garbage collection).

kubectl get pods and kubectl describe pod both read this array; kubectl wait --for=condition=Ready is the scriptable way to block on it. Source: Kubernetes Docs — Pod Lifecycle: Pod conditions.

Container states and restartPolicy

Independently of the Pod-level phase, each container has its own state (surfaced under status.containerStatuses[].state): Waiting (not yet running — pulling its image, or waiting on a previous container state; reason holds strings such as ImagePullBackOff or CrashLoopBackOff), Running, or Terminated (with an exitCode and, if it crashed, a reason such as OOMKilled).

spec.restartPolicy (a Pod-level field, applying to every ordinary container in the Pod) decides what happens next after a container terminates:

restartPolicy Behaviour

Always (default)

Restart on any termination, success or failure. What every Deployment- and StatefulSet-managed Pod uses, because those controllers expect their Pods to run forever.

OnFailure

Restart only on a non-zero exit code; a container that exits 0 stays Terminated. One of the two values a Job Pod template may use — a Job has no default and must set OnFailure or Never explicitly (see Jobs and CronJobs).

Never

Never restart a terminated container; a failed container is left Terminated for inspection, and the Job controller (not the kubelet) decides whether to create a replacement Pod.

A restarted container backs off exponentially (10s, 20s, 40s, …​ capped, by default, at five minutes) so a container that fails immediately does not spin the node’s CPU and API server with restart attempts; kubectl describe pod shows this as CrashLoopBackOff while the container waits out its backoff window. Source: Kubernetes Docs — Pod Lifecycle: restart policy.

stateDiagram-v2 [*] --> Pending Pending --> Running: scheduled, image pulled,\ncontainer(s) started Pending --> Failed: scheduling or admission\nfailure with no retry Running --> Succeeded: every container exits 0,\nrestartPolicy != Always Running --> Failed: a container exits non-zero,\nrestartPolicy Never or exhausted Running --> Running: container restarted\n(restartPolicy Always/OnFailure) Succeeded --> [*] Failed --> [*]

Inside the Running phase, each container moves through its own states, which is where a crash loop shows up while the Pod phase stays Running:

stateDiagram-v2 [*] --> Waiting: image pull,\ninit containers Waiting --> Running: container started Running --> Terminated: process exits\n(exitCode, reason) Terminated --> Waiting: restarted with back-off\n(shown as CrashLoopBackOff) Terminated --> [*]: no restart

Init containers, native sidecars, multi-container patterns, ephemeral containers and static Pods

Init containers

spec.initContainers is a separate, ordered list of containers that run to completion, one after another, before any of spec.containers starts. Each must exit 0 before the next begins; if one fails, the kubelet retries it according to restartPolicy (the Pod stays Pending throughout). They exist for setup that the application container itself should not have to know how to do: waiting for a dependency to become reachable, running a one-shot migration, or — the pattern Book 2 ch. 5’s Redis example uses — writing generated configuration files into a shared emptyDir volume that the main container then mounts read-only.

spec:
  initContainers:
    - name: wait-for-postgres
      image: ghcr.io/example/bookshelf-toolbox:1.0.0
      command: ["sh", "-c", "until nc -z postgres-0.postgres.bookshelf.svc.cluster.local 5432; do sleep 2; done"]
  containers:
    - name: catalog-api
      image: ghcr.io/example/bookshelf-catalog-api:1.0.0

Native sidecar containers

An init container can also be marked restartPolicy: Always, which turns it into a native sidecar: it still starts (and must reach a running state) before the main containers, but it is not required to exit before they start, and the kubelet keeps it running and restarts it independently for the Pod’s whole lifetime — and, on shutdown, stops it after every main container has terminated, so it stays available to the very end (for example to flush logs or proxy a final health check). This feature graduated to stable in Kubernetes 1.33 (it was first available, behind a feature gate, in 1.28), and is now the recommended way to run a log shipper, a service-mesh proxy or a metrics exporter alongside an application container, without the ordering problems an ordinary spec.containers entry used to have (a sidecar in spec.containers starts in no guaranteed order relative to the app container, and readiness probes on the whole Pod could pass before the sidecar was actually ready).

spec:
  initContainers:
    - name: log-shipper
      image: ghcr.io/example/bookshelf-log-shipper:1.0.0
      restartPolicy: Always          # marks this init container as a native sidecar
      volumeMounts:
        - name: logs
          mountPath: /var/log/bookshelf
  containers:
    - name: catalog-api
      image: ghcr.io/example/bookshelf-catalog-api:1.0.0
      volumeMounts:
        - name: logs
          mountPath: /var/log/bookshelf
  volumes:
    - name: logs
      emptyDir: {}

Multi-container patterns

Native sidecars formalise a scheduling guarantee; the pattern a sidecar container implements is still one of a few well-known shapes, all built from the same "extra container sharing this Pod’s network and volumes" primitive:

Pattern What the extra container does

Sidecar

Adds a cross-cutting capability to the main container without changing its image — a log shipper (above), a service-mesh proxy (see Cluster Networking, CNI and Service Mesh), or a certificate refresher writing into a shared volume.

Ambassador

Proxies the main container’s outbound (or inbound) network traffic through localhost, so the application talks to localhost:6379 and the ambassador container handles service discovery, retries or TLS to the real backend.

Adapter

Normalises the main container’s output for a consumer that expects a different shape — for example, rewriting an application’s own log or metrics format into the shape a cluster-wide collector expects, so the application itself stays unaware of that collector.

Ephemeral containers

spec.ephemeralContainers is a fourth container list, added to an already-running Pod via the pods/ephemeralcontainers subresource rather than at creation time, purely for interactive troubleshooting: it has no ports, no probes, no resource guarantees, and (unlike a native sidecar) is never restarted. kubectl debug is the everyday interface to it — see Debugging and Troubleshooting for the full command set, including attaching a debug container to a minimal distroless image that ships no shell of its own. Source: Kubernetes Docs — Ephemeral Containers.

Static Pods

A static Pod is a Pod the kubelet manages directly from a manifest file on its own node’s filesystem (by default /etc/kubernetes/manifests), completely bypassing the API server and the scheduler: the kubelet watches that directory and starts, restarts or removes Pods as files appear, change or disappear, with no controller and no kubectl apply involved. The kubelet still mirrors each one onto the API server as a read-only mirror Pod so it shows up in kubectl get pods, but kubectl delete on that mirror Pod only removes the mirror — the kubelet recreates it immediately, because the real source of truth is still the file on disk. Static Pods are how kubeadm itself runs kube-apiserver, etcd, kube-scheduler and kube-controller-manager as ordinary Pods on the control plane’s own nodes, without needing a running control plane to bootstrap the control plane (see Cluster Setup and Administration); outside that bootstrapping role, application workloads should use a Deployment or DaemonSet instead, since a static Pod has none of a controller’s rescheduling, rolling-update or multi-node consistency guarantees. Source: Kubernetes Docs — Static Pods.

Environment variables, envFrom, the Downward API, hostAliases and image pulling

Environment variables and envFrom

Each container’s env list sets variables directly (value) or from another object’s field (valueFrom), without the application ever reading Kubernetes objects itself — the mechanism Book 1 ch. 3 already relies on for service discovery before cluster DNS existed, and still the simplest way to hand a container one or two values:

valueFrom kind Reads

configMapKeyRef

One key from a ConfigMap (see ConfigMaps).

secretKeyRef

One key from a Secret (see Secrets).

fieldRef

One field of the Pod’s own spec/metadata — the Downward API, below.

resourceFieldRef

A container’s own resource request or limit, e.g. limits.memory — also Downward API.

envFrom injects every key of a ConfigMap or Secret at once, each key becoming an env-var name (optionally prefixed), which is the pattern to reach for once a container needs more than a couple of individually-named values:

spec:
  containers:
    - name: catalog-api
      image: ghcr.io/example/bookshelf-catalog-api:1.0.0
      envFrom:
        - configMapRef:
            name: catalog-api-config
        - secretRef:
            name: catalog-api-secrets
      env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name

The Downward API

The Downward API is the general form of fieldRef/resourceFieldRef: a way to expose a Pod’s own metadata and a container’s own resource limits to itself, either as environment variables (above) or as files in a mounted downwardAPI volume, without the container ever calling back into the API server. Typical fields: metadata.name, metadata.namespace, metadata.uid, metadata.labels, metadata.annotations, spec.nodeName, status.podIP, status.hostIP, and limits.cpu / requests.memory per container. The volume form is the only way to expose labels and annotations (their keys can contain characters an env-var name cannot), and it updates the mounted files automatically if labels or annotations change on a running Pod, unlike a configMapKeyRef env var which is fixed at container start.

spec:
  containers:
    - name: catalog-api
      image: ghcr.io/example/bookshelf-catalog-api:1.0.0
      volumeMounts:
        - name: pod-info
          mountPath: /etc/podinfo
          readOnly: true
  volumes:
    - name: pod-info
      downwardAPI:
        items:
          - path: labels
            fieldRef:
              fieldPath: metadata.labels
          - path: cpu_limit
            resourceFieldRef:
              containerName: catalog-api
              resource: limits.cpu

Hostname, subdomain and hostAliases

A Pod’s own hostname defaults to metadata.name (overridable via spec.hostname), and setting spec.subdomain together with a headless Service of the same name gives it a stable, resolvable DNS name of its own — the exact mechanism StatefulSets builds per-Pod DNS on top of. Separately, spec.hostAliases adds fixed entries to the Pod’s /etc/hosts, useful for pointing at a host that has no DNS entry yet (or overriding one) without touching cluster DNS:

spec:
  hostAliases:
    - ip: "203.0.113.10"
      hostnames:
        - "legacy-inventory.internal"
  containers:
    - name: catalog-api
      image: ghcr.io/example/bookshelf-catalog-api:1.0.0

imagePullPolicy and imagePullSecrets

Every container’s image is subject to imagePullPolicy, which decides whether the kubelet trusts a locally cached image or asks the runtime to pull again — the Kubernetes-side counterpart of the tag and registry choices Registries and Docker Hub covers on the build side:

imagePullPolicy Behaviour

Always

Pull before every start, even if an image with that exact tag is already cached on the node — the default when the tag is :latest or omitted, since a mutable tag can point at different content over time.

IfNotPresent

Pull only if the image is not already present on the node — the default for any other explicit tag, and the right choice for the immutable, versioned tags this section’s Bookshelf manifests use throughout.

Never

Never pull; fail if the image is not already present locally. Used mainly for images built and loaded straight into a local cluster, e.g. kind load docker-image in Getting Started with Kubernetes.

imagePullSecrets names one or more kubernetes.io/dockerconfigjson Secrets (created with kubectl create secret docker-registry) the kubelet presents when pulling from a private registry — the Kubernetes-side equivalent of docker login running once on every node instead of once on a developer’s machine:

kubectl create secret docker-registry ghcr-pull \
  --docker-server=ghcr.io \
  --docker-username=bookshelf-bot \
  --docker-password="$(cat ghcr-pull-token.txt)"
spec:
  imagePullSecrets:
    - name: ghcr-pull
  containers:
    - name: catalog-api
      image: ghcr.io/example/bookshelf-catalog-api:1.0.0
      imagePullPolicy: IfNotPresent

A Deployment’s every Pod needs the same imagePullSecrets entry (or the cluster-wide default set on the namespace’s default ServiceAccount), so a private-registry pull failure shows up as ImagePullBackOff on new Pods rather than as a build-time error. Source: Kubernetes Docs — Pull an Image from a Private Registry and Kubernetes Docs — Images.

A Pod’s anatomy

[pod-anatomy] puts the sections above together: a native-sidecar init container and two ordinary containers, all inside one Pod, sharing its one network namespace (and therefore one IP and localhost) and a volume mounted by more than one of them.

A Pod containing an init sidecar container and two application containers sharing one network namespace and one volume