Pods — The Atomic Unit¶
Overview¶
A Pod is the smallest deployable unit in Kubernetes — not a container, but a wrapper around one or more containers that share network and storage. Every Deployment, StatefulSet, Job, and DaemonSet ultimately creates Pods. Understanding Pod anatomy, lifecycle, and multi-container patterns is essential before managing replicas, Services, or storage.
This tutorial explains what Pods are, how they differ from Docker containers, when to use multi-container Pods, and how to author Pod manifests for labs — while emphasizing that Deployments manage Pods in production.
This is Tutorial 5 in Module 2: Workloads of the REBASH Academy Kubernetes series. Complete kubectl Essentials and Workflows and have a running cluster from Installing Kubernetes and kubectl. Review From Docker to Kubernetes for the Docker-to-Pod mapping. This tutorial follows our documentation standards.
Prerequisites¶
- kubectl Essentials and Workflows
- Installing Kubernetes and kubectl
- From Docker to Kubernetes
- Running Your First Container — container basics
- Running local cluster (minikube or kind) with kubectl configured
Learning Objectives¶
By the end of this tutorial, you will be able to:
- Explain what a Pod is and why Kubernetes wraps containers in Pods
- Author a Pod manifest with containers, ports, env vars, and resource requests
- Describe Pod lifecycle phases and container restart policies
- Implement sidecar and init container patterns at a basic level
- Debug Pod failures using kubectl describe, logs, and events
- Articulate why bare Pods are for labs — Deployments for production
- Map Docker run flags to Pod spec fields
Architecture Diagram¶
A Pod creates a shared execution environment: one IP address, one set of network namespaces, optional shared volumes.
flowchart TB
subgraph POD["Pod — shared context"]
NET["Network namespace<br/>IP: 10.244.1.15"]
VOL["Shared volumes<br/>emptyDir / config"]
subgraph MAIN["Primary container"]
APP["app :8080"]
end
subgraph SIDE["Sidecar container"]
LOG["log-shipper"]
end
subgraph INIT["Init containers<br/>run sequentially first"]
INITC["fetch-config"]
end
end
NODE["Worker Node<br/>kubelet + containerd"]
SVC["Service selector<br/>app=web"]
NODE --> POD
INITC --> APP
APP --> VOL
LOG --> VOL
SVC --> NET Theory¶
What Is a Pod?¶
A Pod is a group of one or more containers with:
- Shared network namespace — one Pod IP; containers communicate via
localhost - Shared storage volumes — mount the same files (logs, config, sockets)
- Co-scheduled placement — always on the same node
The Pod is ephemeral. When it dies, it is replaced with a new Pod (new UID, often new IP). Stable identity requires higher-level controllers (Deployment, StatefulSet).
Pod vs Docker Container¶
| Aspect | Docker container | Kubernetes Pod |
|---|---|---|
| Unit of scheduling | Single container | Pod (1+ containers) |
| Networking | Bridge network, published ports | Pod IP via CNI; Service for stable access |
| Lifecycle manager | dockerd restart policy | kubelet + controller (Deployment) |
| Naming | Container name | Pod name; containers have name field |
| Multi-container | docker run + --network container: hack | Native sidecar pattern |
Docker equivalent from From Docker to Kubernetes:
Maps to a Pod spec with containers, env, and ports — but without -p on the Pod; use a Service for exposure.
Pod Manifest Structure¶
apiVersion: v1
kind: Pod
metadata:
name: my-pod
labels:
app: my-app
spec:
containers:
- name: app
image: myapp:1.0.0
ports:
- containerPort: 8080
env:
- name: LOG_LEVEL
value: info
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
restartPolicy: Always
| Field | Purpose |
|---|---|
metadata.labels | Service selectors, monitoring, affinity |
spec.containers[].image | OCI image — same as Docker |
spec.containers[].ports.containerPort | Documentation + probe target; does not publish externally |
spec.restartPolicy | Always, OnFailure, Never |
spec.nodeName | Bypass scheduler (rare; debugging only) |
spec.serviceAccountName | Identity for API access (RBAC module later) |
Pod Lifecycle Phases¶
| Phase | Meaning |
|---|---|
| Pending | Accepted but not running — scheduling, image pull |
| Running | At least one container started |
| Succeeded | All containers terminated successfully (Jobs) |
| Failed | At least one container failed; none running |
| Unknown | Node communication lost |
Container states: Waiting (reason: ContainerCreating, ImagePullBackOff), Running, Terminated.
flowchart LR
PEND["Pending"]
RUN["Running"]
SUCC["Succeeded"]
FAIL["Failed"]
PEND --> RUN
RUN --> SUCC
RUN --> FAIL
PEND --> FAIL Restart Policies¶
| Policy | Behavior | Typical use |
|---|---|---|
| Always | Restart on any exit | Long-running apps (default for Deployments) |
| OnFailure | Restart only on non-zero exit | Batch, init-heavy Pods |
| Never | No restart | One-shot debug Pods |
Bare Pod with restartPolicy: Always restarts on the same node — but if the node fails, the Pod is gone unless a Deployment recreates it.
Multi-Container Patterns¶
Sidecar¶
Helper container alongside main app — log shipping, proxy, config sync:
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-shipper
image: fluent/fluent-bit:2.2
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
Both containers share emptyDir volume at /var/log/app.
Init Containers¶
Run sequentially before app containers start — wait for dependencies, download assets, migrate DB schema:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db 5432; do sleep 2; done']
containers:
- name: app
image: myapp:1.0
Init failure blocks Pod startup — kubelet retries per restart policy.
Resource Requests and Limits¶
| Field | Effect |
|---|---|
| requests | Scheduler uses for placement; guaranteed minimum |
| limits | cgroup cap — OOM kill if memory exceeded; CPU throttled |
Without requests, scheduler may overcommit nodes — worse than missing Docker --memory limits (see From Docker to Kubernetes).
CPU units: 100m = 0.1 core. Memory: 128Mi, 1Gi.
Why Not Bare Pods in Production?¶
| Bare Pod issue | Deployment solution |
|---|---|
| Deleted Pod never returns | ReplicaSet maintains replica count |
| No rolling updates | Deployment strategy manages rollouts |
| No revision history | rollout history / undo |
| Manual scaling | kubectl scale deployment |
Use bare Pods for learning, one-off debug, and Job patterns — not for stateless web apps.
Pod Networking Recap¶
- Each Pod gets a cluster-routable IP from the CNI plugin
- Containers in Pod share IP — bind to different ports on
localhost - Other Pods reach this Pod via Service ClusterIP or direct Pod IP (discouraged)
hostNetwork: trueuses node network — rare, security-sensitive
Hands-on Lab¶
Step 1 – Create a single-container Pod¶
Command:
mkdir -p ~/k8s-lab/module2/pods
cat <<'EOF' > ~/k8s-lab/module2/pods/nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
tier: frontend
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
memory: "32Mi"
cpu: "50m"
limits:
memory: "64Mi"
cpu: "100m"
restartPolicy: Never
EOF
kubectl apply -f ~/k8s-lab/module2/pods/nginx-pod.yaml
kubectl get pod nginx-pod -w
# Ctrl+C when Running
Explanation: restartPolicy: Never suits lab Pods you manage manually. Observe phase transition Pending → Running.
Expected output:
Step 2 – Inspect Pod details and IP¶
Command:
kubectl describe pod nginx-pod
kubectl get pod nginx-pod -o jsonpath='{.status.podIP}{"\n"}{.status.phase}{"\n"}'
kubectl exec nginx-pod -- curl -s -o /dev/null -w "%{http_code}\n" http://localhost:80
Explanation: Pod IP is reachable from other Pods in the cluster. localhost works inside the container because it shares the Pod network namespace.
Expected output:
Step 3 – Environment variables and commands¶
Command:
cat <<'EOF' > ~/k8s-lab/module2/pods/env-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: env-demo
spec:
containers:
- name: shell
image: busybox:1.36
command: ["sh", "-c", "echo HOST=$HOST VERSION=$VERSION && sleep 3600"]
env:
- name: HOST
value: rebash-lab
- name: VERSION
value: "1.0"
restartPolicy: Never
EOF
kubectl apply -f ~/k8s-lab/module2/pods/env-pod.yaml
kubectl logs env-demo
Explanation: Maps Docker -e flags to env list. command overrides image entrypoint — same as Docker CMD.
Expected output:
Step 4 – Init container pattern¶
Command:
cat <<'EOF' > ~/k8s-lab/module2/pods/init-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
initContainers:
- name: init-myservice
image: busybox:1.36
command: ['sh', '-c', 'echo prepared > /work/data.txt']
volumeMounts:
- name: workdir
mountPath: /work
containers:
- name: app
image: busybox:1.36
command: ['sh', '-c', 'cat /work/data.txt && sleep 3600']
volumeMounts:
- name: workdir
mountPath: /work
volumes:
- name: workdir
emptyDir: {}
restartPolicy: Never
EOF
kubectl apply -f ~/k8s-lab/module2/pods/init-pod.yaml
kubectl get pod init-demo -w
kubectl logs init-demo
Explanation: Init container writes to shared emptyDir; main container reads it. Init runs to completion before app starts.
Expected output:
Step 5 – Sidecar pattern (shared volume)¶
Command:
cat <<'EOF' > ~/k8s-lab/module2/pods/sidecar-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
spec:
containers:
- name: writer
image: busybox:1.36
command: ['sh', '-c', 'while true; do date >> /var/log/app.log; sleep 5; done']
volumeMounts:
- name: logs
mountPath: /var/log
- name: reader
image: busybox:1.36
command: ['sh', '-c', 'tail -f /var/log/app.log']
volumeMounts:
- name: logs
mountPath: /var/log
volumes:
- name: logs
emptyDir: {}
restartPolicy: Never
EOF
kubectl apply -f ~/k8s-lab/module2/pods/sidecar-pod.yaml
sleep 8
kubectl logs sidecar-demo -c reader --tail=3
Explanation: Classic sidecar: writer appends logs; reader tails the same file via shared volume — mimics log shipping without a separate daemonset.
Expected output:
Step 6 – Simulate failure and observe restart policy¶
Command:
cat <<'EOF' > ~/k8s-lab/module2/pods/crash-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: crash-demo
spec:
containers:
- name: flaky
image: busybox:1.36
command: ['sh', '-c', 'echo starting; exit 1']
restartPolicy: OnFailure
EOF
kubectl apply -f ~/k8s-lab/module2/pods/crash-pod.yaml
sleep 5
kubectl get pod crash-demo
kubectl describe pod crash-demo | grep -A6 "Containers:" | head -10
Explanation: Container exits with code 1; OnFailure triggers kubelet restart. Compare with restartPolicy: Never where Pod stays Failed.
Expected output:
Step 7 – Clean up lab Pods¶
Command:
Commands & Code¶
| Command | Description | Example |
|---|---|---|
kubectl apply -f pod.yaml | Create/update Pod | kubectl apply -f nginx-pod.yaml |
kubectl get pod -o yaml | Export live Pod spec | kubectl get pod NAME -o yaml |
kubectl describe pod | Events, conditions, container state | kubectl describe pod NAME |
kubectl logs -c | Logs from specific container | kubectl logs POD -c sidecar |
kubectl exec -c | Exec into specific container | kubectl exec POD -c app -- sh |
kubectl delete pod | Remove Pod | kubectl delete pod NAME |
kubectl explain pod.spec | Field documentation | kubectl explain pod.spec.initContainers |
kubectl get pod -w | Watch status changes | kubectl get pods -w |
Pod quick-debug alias¶
# Usage: kdebug my-pod
kdebug() {
kubectl describe pod "$1"
echo "--- logs ---"
kubectl logs "$1" --all-containers=true --tail=30
}
Common Mistakes¶
Deploying bare Pods for production web apps
A deleted Pod is not recreated. Always use Deployments for stateless apps — covered in the next tutorial in this module.
Expecting containerPort to publish externally
containerPort documents the port; it does not bind host ports. Use Services or Ingress for access — unlike docker run -p.
Putting unrelated services in one Pod
Pods scale as a unit. Do not co-locate unrelated apps — you cannot scale them independently.
Omitting resource requests
Pods without requests may schedule on overloaded nodes and get OOMKilled. Always set requests; set limits for production.
Using latest image tag
Same anti-pattern as Docker — pin semver or digest; unexpected pulls break reproducibility.
Best Practices¶
Use labels on every Pod template
app, version, and component labels enable Service selectors and observability queries.
Prefer Deployments over bare Pods
Even in dev, practice Deployment → Pod workflow to build production habits early.
Use init containers for startup ordering
Replace brittle sleep 30 entrypoints with init containers that probe dependencies.
Keep sidecars lightweight
Sidecars consume Pod resource quotas — account for their CPU/memory in requests and limits.
Debug with describe first
Pod Events reveal ImagePullBackOff, failed mounts, and probe failures faster than guessing from STATUS column alone.
Troubleshooting¶
| Issue | Cause | Solution |
|---|---|---|
| Pending | No schedulable node, resources, taints | kubectl describe pod; check node capacity |
| ImagePullBackOff | Wrong tag, private registry | Verify image name; add imagePullSecrets |
| CrashLoopBackOff | App exits on start | kubectl logs --previous; fix command/config |
| Init:Error | Init container failed | kubectl logs POD -c INIT_NAME |
| OOMKilled | Memory limit exceeded | Increase limit or fix memory leak |
| Invalid manifest | YAML indentation, wrong apiVersion | kubectl apply --dry-run=server -f |
| Pod Running but no response | App listens wrong port | Check containerPort vs app bind address |
| Two containers port conflict | Same port in shared network | Bind different ports on localhost |
Summary¶
- A Pod wraps one or more containers sharing network and volumes — the atomic unit of scheduling in Kubernetes
- Pod lifecycle phases: Pending → Running → Succeeded/Failed; container states include Waiting, Running, Terminated
- Init containers run sequentially before app containers; sidecars run alongside the main container sharing volumes
- restartPolicy controls kubelet restarts; Deployments provide replica management and self-healing across node failures
- resource requests/limits affect scheduling and cgroup enforcement — always set requests in production
- Bare Pods are for learning and Jobs; next in the track: Deployments for replicated workloads
- Continue to Deployments — Managing Replicated Pods when published in this module
Interview Questions¶
- What is a Pod, and why does Kubernetes use Pods instead of scheduling containers directly?
- How do containers within a Pod communicate with each other?
- Explain the difference between init containers and sidecar containers.
- What are Pod phases, and what does Pending indicate?
- Compare restartPolicy values: Always, OnFailure, Never.
- What is the purpose of resource requests vs limits?
- Why should production apps avoid bare Pods?
- How does a Pod get its IP address?
- Map Docker
-e,-p, and--restartto Pod spec fields. - What happens to Pod data when a Pod is deleted?
Sample Answers (Questions 1, 3, and 7)
Q1 — Pod definition: A Pod is the smallest deployable unit in Kubernetes — a group of one or more containers that share a network namespace (one IP), IPC, and optional volumes. Kubernetes schedules Pods, not individual containers, to enable co-located helper patterns (sidecars, init) and simplify networking. Containers in a Pod always run on the same node.
Q3 — Init vs sidecar: Init containers run to completion in sequence before any app container starts — ideal for setup tasks like waiting for a database or downloading config. Sidecar containers run concurrently with the main app container for the Pod's entire lifecycle — ideal for log shipping, proxies, or sync loops. Both share volumes and localhost networking within the Pod.
Q7 — Bare Pods in production: Bare Pods have no controller to recreate them if deleted or evicted. Node failure permanently removes them until manual intervention. Deployments (via ReplicaSets) maintain desired replica count, support rolling updates, and provide rollback history — essential for production availability and release management.
Related Tutorials¶
- kubectl Essentials and Workflows (previous)
- Deployments — Managing Replicated Pods (next in Module 2)
- From Docker to Kubernetes
- Installing Kubernetes and kubectl
- Kubernetes – Category Overview
- Learning Paths – DevOps Engineer