Kubernetes Pods

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

What a Pod is and why Kubernetes schedules pods rather than containers. Shared networking and storage, and debugging Pending and CrashLoopBackOff.

A Pod is the smallest thing Kubernetes will schedule. You can't ask Kubernetes to run a container. You ask it to run a Pod, and the Pod contains one or more containers that are guaranteed to land on the same node and share an network identity.

Almost every Pod in production has exactly one application container. The ability to hold several exists for the cases where two processes are genuinely inseparable, not as a general packaging mechanism.

Why a wrapper around containers at all?

Because some things must run together, and "together" has to mean something precise. A log shipper that reads files the application writes needs the same filesystem. A proxy that intercepts the application's traffic needs the same network interface. Scheduling them independently would mean they might land on different machines, and the arrangement would fall apart.

A Pod makes co-location a guarantee rather than a hope.

What containers in a Pod actually share

SharedNot shared
Network namespace (one IP, one port space)Filesystem (unless you mount a shared volume)
localhost between containersProcess namespace (by default)
Mounted volumes you declareCPU and memory limits (set per container)
Lifecycle (scheduled and deleted as a unit)Container restarts: individual containers restart alone

The shared network namespace has one consequence worth internalising: two containers in the same Pod cannot listen on the same port. They share a port space, so a second container binding :8080 fails exactly as it would on a single host.

Pod anatomy

Pod (shared network namespace)

Init Container

Runs to completion before main containers start

Container 1. Main app

Your application process

Container 2. Sidecar

e.g. log forwarder, proxy

Shared network

Same IP & ports

Shared volumes

emptyDir / PVC

Behind the scenes a small "pause" container holds the network namespace open so application containers can restart without the Pod losing its IP.

Pods are disposable, and the IP goes with them

Every Pod gets its own cluster-routable IP. That IP belongs to the Pod, not to your application, and it is gone the moment the Pod is replaced. During a deploy, a node drain, an eviction, or a crash past the restart limit.

kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP           NODE
payments-api-7d4f-x8k2   1/1     Running   0          4m    10.244.1.17  node-1
payments-api-7d4f-m3p9   1/1     Running   0          4m    10.244.2.31  node-2

Nothing should ever hardcode 10.244.1.17. That is what a Service is for. It provides a stable name and virtual IP in front of whatever Pods currently exist.

Writing a Pod

apiVersion: v1
kind: Pod
metadata:
  name: payments-api
  labels:
    app: payments-api
spec:
  containers:
    - name: api
      image: ghcr.io/acme/payments-api:1.4.2
      ports:
        - containerPort: 8080
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
        limits:
          memory: 512Mi
      readinessProbe:
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 5
      env:
        - name: LOG_LEVEL
          value: info

Three details in that manifest are worth defending in review:

Pin the image tag. :1.4.2, never :latest. A mutable tag means two Pods from the same manifest can run different code, and a restart can silently change your application version.

Always set a requests value. Without it the scheduler assumes the Pod needs nothing and will happily pack it onto a node with no room, where it competes for CPU and gets evicted first under memory pressure.

Set a memory limit but think twice about a CPU limit. Exceeding a memory limit gets the container killed (OOMKilled), which is what you want. An untethered memory leak takes down the node otherwise. CPU limits work differently: they throttle rather than kill, and an aggressive CPU limit adds latency to an application that would otherwise be fine.

Multi-container patterns

Three patterns justify a second container:

Init containers run to completion, in order, before any application container starts. Use them for migrations, waiting on a dependency, or fetching configuration.

spec:
  initContainers:
    - name: run-migrations
      image: ghcr.io/acme/payments-api:1.4.2
      command: ["./migrate", "up"]
  containers:
    - name: api
      image: ghcr.io/acme/payments-api:1.4.2

If an init container fails, the Pod retries it and the application never starts, which is usually correct. A Pod stuck in Init:0/1 means the init container is failing or waiting; kubectl logs <pod> -c run-migrations tells you which.

Sidecars run alongside the application for its whole life: a log shipper, a metrics exporter, a service mesh proxy. Modern Kubernetes models these as init containers with restartPolicy: Always, which fixes a long-standing problem where a Job's sidecar kept running after the main container finished, so the Job never completed.

spec:
  initContainers:
    - name: log-shipper
      image: fluent/fluent-bit:3.1
      restartPolicy: Always   # makes this a native sidecar

Adapters reshape output. For example, translating an application's custom metrics format into something Prometheus can scrape.

If you're reaching for a second container and it isn't one of these, you probably want a second Pod.

Lifecycle

A Pod's phase is a coarse summary:

PhaseMeaning
PendingAccepted, not yet running (being scheduled, or pulling images)
RunningBound to a node, at least one container running
SucceededAll containers exited 0 and will not restart
FailedAll containers terminated, at least one in failure
UnknownThe node stopped reporting

restartPolicy controls what happens when a container exits: Always (the default, and what Deployments use), OnFailure, or Never. It applies to containers within the Pod. A Pod is never "restarted", it is replaced.

Debugging: the three statuses you will actually see

kubectl describe pod is the right first command for all of them, because the Events section at the bottom usually states the cause outright.

kubectl describe pod payments-api-7d4f-x8k2

Pending: nothing will schedule it. Read the events:

Warning  FailedScheduling  0/3 nodes are available: 3 Insufficient cpu.

Common causes: requests larger than any node can satisfy, no node matching a nodeSelector or affinity rule, a taint with no matching toleration, or an unbound PersistentVolumeClaim.

ImagePullBackOff / ErrImagePull: the kubelet cannot fetch the image. Almost always a typo in the image name, a tag that does not exist, or a private registry with no imagePullSecret. The events name the registry error verbatim.

CrashLoopBackOff: the container starts, exits, and Kubernetes restarts it with growing backoff (10s, 20s, 40s, up to five minutes). This is a symptom, never a cause. The cause is in the logs of the run that failed:

# Logs from the previous, crashed container, not the one starting now
kubectl logs payments-api-7d4f-x8k2 --previous

That --previous flag is the single most useful thing to know about debugging Pods. Without it you get the logs of a container that has only just started and has nothing to say yet.

Typical causes, in rough order of frequency: a missing environment variable or config file, a failing dependency at startup, an OOMKilled container whose memory limit is too low, or a liveness probe that starts before the application finishes booting and kills it repeatedly.

# Was it the memory limit?
kubectl get pod payments-api-7d4f-x8k2 -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'

Don't create bare Pods

A Pod you create directly is not managed by anything. If the node fails, the Pod is gone and nothing recreates it. There is no self-healing, no scaling, no rolling update.

Use a Deployment for stateless applications, a StatefulSet for anything needing stable identity or storage, a DaemonSet for one-per-node agents, and a Job or CronJob for work that finishes. Each of these creates Pods for you.

Bare Pods are for debugging:

kubectl run tmp-shell --rm -it --image=nicolaka/netshoot -- bash

Explore further in this section

Sub-topicWhat you will learn
Pod ManifestWrite and read complete Pod YAML
Pod LifecyclePhases, container states, and restart behaviour
Multi-container PodsSidecars, init containers, and shared volumes
ProbesLiveness, readiness, and startup probes

Quick reference

TaskCommand
List with IPs and nodeskubectl get pods -o wide
Full detail and eventskubectl describe pod <name>
Logskubectl logs <name>
Logs from a crashed runkubectl logs <name> --previous
Logs from one containerkubectl logs <name> -c <container>
Shell into a containerkubectl exec -it <name> -- sh
Watch status changeskubectl get pods -w
Delete immediatelykubectl delete pod <name> --grace-period=0 --force
Full YAML as storedkubectl get pod <name> -o yaml

Key takeaways

  • A Pod is the scheduling unit, and containers inside one share a network namespace, an IP, and any volumes you declare, so they cannot bind the same port.
  • Pod IPs are disposable. Never hardcode one; put a Service in front.
  • Always set resource requests, or the scheduler treats the Pod as free and places it badly.
  • Memory limits kill, CPU limits throttle. Set memory limits; be cautious with CPU limits.
  • CrashLoopBackOff is a symptom. kubectl logs --previous shows the run that actually failed.
  • Never run bare Pods in production: a Deployment, StatefulSet, DaemonSet or Job gives you self-healing.

Check your understanding

Exercise

Two containers, one port

A Pod defines two containers, both configured to listen on port 8080. What happens?

Exercise

Debugging CrashLoopBackOff

A Pod is in CrashLoopBackOff. You run kubectl logs and get no output at all. What is the most useful next command?


Frequently Asked Questions

What is the difference between a Pod and a container?

A container is a running image with its own filesystem and process. A Pod is the Kubernetes object that wraps one or more containers and gives them a shared network namespace, a shared IP, and a shared lifecycle on a single node. Kubernetes schedules Pods, not containers. You cannot ask it to place a container directly. In practice most Pods hold exactly one container, and the multi-container capability exists for processes that genuinely cannot be separated, such as an application and its log shipper.

Why is my Pod stuck in Pending?

Nothing has been able to schedule it. Run kubectl describe pod <name> and read the Events section, which states the reason explicitly. The frequent causes are resource requests larger than any node can satisfy, no node matching a nodeSelector or affinity rule, a node taint with no matching toleration on the Pod, and a PersistentVolumeClaim that has not bound. A message like 0/3 nodes are available: 3 Insufficient cpu means exactly what it says. Either lower the requests or add capacity.

What does CrashLoopBackOff mean and how do I fix it?

It means the container starts, exits, and Kubernetes keeps restarting it with an increasing delay between attempts, up to five minutes. It is a symptom rather than a diagnosis. Read the logs of the run that failed with kubectl logs <pod> --previous, since plain kubectl logs shows the instance that has only just started. The usual causes are a missing environment variable or config file, a dependency unavailable at startup, an OOMKilled container whose memory limit is too low, and a liveness probe firing before the application has finished booting.

Can two containers in the same Pod use the same port?

No. They share one network namespace and therefore one port space, so the second process to bind fails with "address already in use", exactly as it would on a single machine. Give each container a distinct port. This same shared namespace is what lets containers in a Pod reach each other on localhost with no service discovery.

Should I ever create a Pod directly?

Only for debugging. A bare Pod has no controller behind it, so if its node fails or it is evicted, nothing recreates it. There is no self-healing, no scaling, and no rolling update. For real workloads use a Deployment for stateless services, a StatefulSet where stable identity or storage matters, a DaemonSet for per-node agents, and a Job or CronJob for work that completes. A throwaway debugging Pod via kubectl run --rm -it is the legitimate exception.

Why does my Pod keep getting OOMKilled?

The container exceeded its memory limit and the kernel killed it. Confirm it with kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}', which returns OOMKilled. Either the limit is genuinely too low for the workload, or the application has a memory leak that a higher limit will only postpone. Check actual usage with kubectl top pod before raising the number, and be aware that runtimes with their own heap settings, the JVM especially, need to be told about the container limit or they will size their heap against the node's total memory instead.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources