Kubernetes Pods
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
| Shared | Not shared |
|---|---|
| Network namespace (one IP, one port space) | Filesystem (unless you mount a shared volume) |
localhost between containers | Process namespace (by default) |
| Mounted volumes you declare | CPU 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
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:
| Phase | Meaning |
|---|---|
Pending | Accepted, not yet running (being scheduled, or pulling images) |
Running | Bound to a node, at least one container running |
Succeeded | All containers exited 0 and will not restart |
Failed | All containers terminated, at least one in failure |
Unknown | The 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-topic | What you will learn |
|---|---|
| Pod Manifest | Write and read complete Pod YAML |
| Pod Lifecycle | Phases, container states, and restart behaviour |
| Multi-container Pods | Sidecars, init containers, and shared volumes |
| Probes | Liveness, readiness, and startup probes |
Quick reference
| Task | Command |
|---|---|
| List with IPs and nodes | kubectl get pods -o wide |
| Full detail and events | kubectl describe pod <name> |
| Logs | kubectl logs <name> |
| Logs from a crashed run | kubectl logs <name> --previous |
| Logs from one container | kubectl logs <name> -c <container> |
| Shell into a container | kubectl exec -it <name> -- sh |
| Watch status changes | kubectl get pods -w |
| Delete immediately | kubectl delete pod <name> --grace-period=0 --force |
| Full YAML as stored | kubectl 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.
CrashLoopBackOffis a symptom.kubectl logs --previousshows the run that actually failed.- Never run bare Pods in production: a Deployment, StatefulSet, DaemonSet or Job gives you self-healing.
Check your understanding
Two containers, one port
A Pod defines two containers, both configured to listen on port 8080. What happens?
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.