02 - Pods and Workloads
Why this matters
The Pod is the smallest unit Kubernetes schedules and runs — every workload object you'll meet later (Deployments, in module 03; and beyond) is ultimately a machine for creating and managing Pods. If you don't understand Pods solidly — what's inside one, how it starts, how it fails — every later module's troubleshooting will feel like guesswork.
Concepts
A Pod is one or more containers that are always scheduled together, on
the same node, sharing a network namespace and (optionally) storage.
This is the key difference from plain docker run: with Docker Compose,
each service is usually its own container with its own IP; in a Pod,
every container inside it shares one IP address and one localhost —
container A can reach container B on localhost:<port> as if they were
two processes on the same machine. Most Pods have exactly one container;
multi-container Pods are for tightly-coupled helpers (a "sidecar," like a
log shipper that reads files the main container writes).
┌─────── Pod ───────┐
one IP address ────►│ 10.244.0.7 │
│ ┌──────┐ ┌──────┐ │
│ │ main │ │sidecar│ │ both reach each other
│ │ ctr │ │ ctr │ │ on localhost:<port>
│ └──┬───┘ └──┬───┘ │
│ └────┬────┘ │
│ shared volume │ (emptyDir, etc.)
│ shared network ns │
└────────────────────┘
A Pod is meant to be disposable. Pods don't move between nodes, and if a Pod dies, nothing brings back that exact Pod (as you saw in module 01's exercise 9, only its containers get restarted by the kubelet, not the Pod object itself if the whole Pod is deleted). That's intentional — you're not meant to create bare Pods directly in real usage; you're meant to use a workload controller (module 03) that creates and replaces Pods for you. We start with bare Pods here anyway because you need to understand what's inside one before wrapping it in a controller.
The Pod spec describes desired state: which container image(s) to
run, what commands/args, what ports, what environment variables, what
resource requests/limits, and what health checks to run. This is the
YAML equivalent of the parts of a docker run command you already know
— -p, -e, --memory, --entrypoint — just expressed declaratively.
Resource requests and limits tell the scheduler and kubelet how much
CPU/memory a container needs (requests, used for scheduling decisions —
"does any node have this much spare capacity?") and how much it's allowed
to use at most (limits, enforced at runtime). Exceed a memory limit and
the container is killed (OOMKilled); exceed a CPU limit and the container
is throttled, not killed. Without requests, the scheduler can pack Pods
onto a node with no guarantee they'll actually fit once busy.
Probes are how Kubernetes checks whether a container is actually healthy, instead of just "is the process still running":
- A liveness probe answers "is this container stuck/dead and should be restarted?" Fail it repeatedly, and the kubelet kills and restarts the container.
- A readiness probe answers "is this container ready to receive traffic right now?" Fail it, and the Pod is removed from any Service's routing (module 04) without being restarted — useful for a container that's alive but still warming up (loading a large model, waiting on a database connection).
- A startup probe gives a slow-starting container extra time before liveness probes start counting against it.
probe what it gates on failure
───────────────────────────────────────────────────────────
startup ──► holds off liveness/readiness until app booted
liveness ──► kubelet KILLS + RESTARTS the container
readiness ──► Pod REMOVED from Service endpoints (no restart)
start ──► [startup passes] ──► [readiness passes] ──► gets traffic
│ │
liveness keeps container alive the whole time
Pod phases: Pending (accepted by the API, not yet fully scheduled
or its images not yet pulled), Running (at least one container
running), Succeeded/Failed (all containers exited, terminally), and
Unknown. kubectl get pods also shows container-level states you'll
use constantly for debugging: ContainerCreating, CrashLoopBackOff
(a container keeps crashing and Kubernetes is backing off restart
attempts with increasing delay), ImagePullBackOff (the image couldn't
be pulled and it's backing off retrying), OOMKilled (killed for
exceeding its memory limit).
Init containers run to completion, in order, before the Pod's main containers start — useful for "wait for the database to be reachable" or "populate a config file" steps that must finish first.
Command reference
| Command | What it does | Example |
|---|---|---|
kubectl run <name> --image=<img> |
Imperatively creates a single Pod (quick, not for real workloads) | kubectl run nginx --image=nginx |
kubectl apply -f <file> |
Creates/updates objects from a YAML manifest (declarative, preferred) | kubectl apply -f pod.yaml |
kubectl get pods |
Lists Pods and their phase/restarts/age | kubectl get pods -o wide |
kubectl describe pod <name> |
Shows full detail including events — the #1 debugging command | kubectl describe pod nginx |
kubectl logs <pod> |
Shows a container's stdout/stderr logs | kubectl logs nginx -c app --previous |
kubectl exec -it <pod> -- <cmd> |
Runs a command inside a running container | kubectl exec -it nginx -- sh |
kubectl delete pod <name> |
Deletes a Pod | kubectl delete pod nginx |
kubectl port-forward pod/<name> <local>:<pod> |
Forwards a local port to a port in the Pod | kubectl port-forward pod/nginx 8080:80 |
kubectl get pods -o yaml |
Dumps the full live object as YAML (including status the cluster added) | kubectl get pod nginx -o yaml |
spec.containers[].resources.requests |
Declares minimum CPU/memory needed (scheduling hint) | requests: {cpu: "100m", memory: "128Mi"} |
spec.containers[].resources.limits |
Declares the hard cap on CPU/memory | limits: {cpu: "250m", memory: "256Mi"} |
spec.containers[].livenessProbe |
Defines how to check "is this container alive" | see exercises |
spec.containers[].readinessProbe |
Defines how to check "is this container ready for traffic" | see exercises |
spec.initContainers |
List of containers that must complete before main containers start | see exercises |
Hands-on exercises
Use namespace demo on your kind-learning cluster (kubectl create namespace demo if you deleted it in module 01;
kubectl config set-context --current --namespace=demo to default into
it).
1. Your first Pod manifest
# pod-nginx.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
kubectl apply -f pod-nginx.yaml
kubectl get pods
kubectl describe pod nginx
Expected: STATUS: Running, 1/1 ready. In describe, check the
Events section for Pulled, Created, Started.
2. Reach the Pod directly
kubectl port-forward pod/nginx 8080:80
In another terminal:
curl localhost:8080
Expected: nginx's default welcome page HTML. Ctrl+C the port-forward when done.
3. Logs and exec
kubectl logs nginx
kubectl exec -it nginx -- sh -c "hostname; ls /usr/share/nginx/html"
Expected: logs show nginx access/startup lines; hostname shows the
Pod's name — proof it's the same "machine" you curled.
4. Resource requests and limits
# pod-limited.yaml
apiVersion: v1
kind: Pod
metadata:
name: limited
spec:
containers:
- name: app
image: nginx:1.27
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
kubectl apply -f pod-limited.yaml
kubectl describe pod limited
Expected: a Limits/Requests section in the describe output matching
the YAML. Check kubectl describe node learning-control-plane afterward
— the Allocated resources section now reflects this Pod's requests.
5. Liveness and readiness probes
# pod-probes.yaml
apiVersion: v1
kind: Pod
metadata:
name: probed
spec:
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
kubectl apply -f pod-probes.yaml
kubectl get pod probed --watch
Expected: READY goes from 0/1 to 1/1 once the readiness probe
passes. Ctrl+C to stop watching.
6. Multi-container Pod (sidecar) with shared storage
# pod-sidecar.yaml
apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
spec:
volumes:
- name: shared-logs
emptyDir: {}
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c", "while true; do date >> /var/log/app/out.log; sleep 2; done"]
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
- name: reader
image: busybox:1.36
command: ["sh", "-c", "tail -f /var/log/app/out.log"]
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
kubectl apply -f pod-sidecar.yaml
kubectl logs sidecar-demo -c reader
Expected: timestamps written by writer, read back by reader — proof
the two containers share the emptyDir volume even though they're
separate containers. Also try:
kubectl exec -it sidecar-demo -c writer -- hostname
kubectl exec -it sidecar-demo -c reader -- hostname
Expected: identical hostname — same Pod, same network namespace, two containers.
7. Init containers
# pod-init.yaml
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
initContainers:
- name: wait-for-setup
image: busybox:1.36
command: ["sh", "-c", "echo 'doing setup...'; sleep 5; echo 'setup done'"]
containers:
- name: app
image: nginx:1.27
kubectl apply -f pod-init.yaml
kubectl get pod init-demo --watch
Expected: Pod shows Init:0/1 for ~5 seconds, then transitions to
PodInitializing briefly, then Running. Check:
kubectl logs init-demo -c wait-for-setup
8. Diagnose and fix: CrashLoopBackOff
# pod-crash.yaml
apiVersion: v1
kind: Pod
metadata:
name: crasher
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo 'starting up'; exit 1"]
kubectl apply -f pod-crash.yaml
kubectl get pod crasher --watch
Expected: STATUS cycles ContainerCreating → Error/Completed →
CrashLoopBackOff, with RESTARTS climbing. Diagnose:
kubectl describe pod crasher
kubectl logs crasher
kubectl logs crasher --previous
Expected: describe's Events shows Back-off restarting failed container; logs shows starting up then the container exits.
The fix here is understanding the command intentionally exits 1 — in
real life you'd fix the application bug or the wrong command. Fix it by
replacing the command with one that stays running, and re-apply:
kubectl delete pod crasher
# pod-crash-fixed.yaml
apiVersion: v1
kind: Pod
metadata:
name: crasher
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo 'starting up'; sleep 3600"]
kubectl apply -f pod-crash-fixed.yaml
kubectl get pod crasher
Expected: Running, RESTARTS: 0.
9. Diagnose and fix: ImagePullBackOff
# pod-badimage.yaml
apiVersion: v1
kind: Pod
metadata:
name: bad-image
spec:
containers:
- name: app
image: nginx:this-tag-does-not-exist
kubectl apply -f pod-badimage.yaml
kubectl get pod bad-image --watch
Expected: STATUS becomes ErrImagePull then ImagePullBackOff.
Diagnose:
kubectl describe pod bad-image
Expected: an event like Failed to pull image ... manifest unknown.
Fix by correcting the tag and re-applying:
kubectl delete pod bad-image
# pod-badimage.yaml (corrected)
apiVersion: v1
kind: Pod
metadata:
name: bad-image
spec:
containers:
- name: app
image: nginx:1.27
kubectl apply -f pod-badimage.yaml
kubectl get pod bad-image
Expected: Running.
10. Clean up
kubectl delete pod nginx limited probed sidecar-demo init-demo crasher bad-image
kubectl get pods
Expected: No resources found in demo namespace.
Independent challenge
No YAML or commands given here — figure it out yourself using what you know from this module and earlier ones.
Task: Write a single Pod manifest for a web container that (a) declares
both CPU and memory requests and limits, (b) does not start serving until
an init container has finished a short "warm-up" step, and (c) is only
reported Ready once an HTTP health check against its serving port
passes — but is not restarted merely for being slow to warm up. Apply it
into the demo namespace, then prove each of the three behaviors actually
took effect by observing the Pod as it starts and inspecting its
describe output. This combines this module's probes/init-containers/
resources with the namespace discipline from module 01.
Stuck? One hint
You want an initContainers entry plus a readinessProbe (not a
liveness probe) for the "ready but never restarted for slowness" part;
watch the phase transitions with kubectl get pod <name> --watch and
confirm the limits in kubectl describe pod.
Common mistakes & troubleshooting
- Creating bare Pods for real workloads: if the Pod dies (node reboot, eviction, manual delete), nothing recreates it. Bare Pods are for learning/one-off debugging; module 03's Deployments are what you actually use.
- Confusing liveness and readiness: a failing liveness probe kills and restarts the container; a failing readiness probe just pulls it out of Service traffic. Using liveness where you meant readiness can cause a slow-starting-but-healthy app to be restarted in a loop right as it's about to become ready.
- No resource requests set: the scheduler can then pack far more Pods onto a node than it can actually handle once they're all under load, leading to node-wide slowdowns or evictions.
- Forgetting
-c <container>in a multi-container Pod:kubectl logs/execneed-cto pick a container once there's more than one; omitting it errors or (forlogs) fails with an ambiguous-container message. - Reading
kubectl logsinstead of--previousafter a restart: once a container has restarted, plainlogsshows the new instance's (possibly empty) output — the crash's logs are in--previous. - Not checking
kubectl describe's Events section first: it's the fastest way to see why something is stuck, before diving into logs.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- What do all containers inside the same Pod share that containers in separate Pods do not?
- What's the practical difference between a failed liveness probe and a failed readiness probe?
- What happens to a container that exceeds its memory
limit? What happens if it exceeds its CPUlimit? - What's the difference between
kubectl logs <pod>andkubectl logs <pod> --previous? - Why are bare Pods generally not used directly for real workloads?
- Given a Pod stuck in
ImagePullBackOff, what are the two commands you should run first to diagnose it, and what are you looking for in each? - What do init containers guarantee about ordering?
Show answers
- A network namespace (same IP, can reach each other on
localhost) and, if configured, shared volumes. - A failed liveness probe causes the kubelet to kill and restart the container; a failed readiness probe just removes the Pod from Service endpoints (no restart) until it passes again.
- Exceeding the memory limit gets the container OOMKilled (terminated); exceeding the CPU limit throttles it (slows it down) rather than killing it.
- Plain
logsshows the current/running container instance's output;--previousshows the output of the instance before the most recent restart — essential for seeing why a crash happened. - If a bare Pod's node fails or the Pod is deleted/evicted, nothing recreates it — there's no controller watching over it, unlike Deployments/ReplicaSets which continuously ensure a desired number of replicas exist.
kubectl describe pod <name>(check Events for the exact pull failure reason/message) andkubectl logs <name>if it ever did start (usually it won't have, for a pure image-pull failure).- Init containers run one at a time, in the order listed, and each must complete successfully before the next one (or the main containers) starts.
Further reading & sources
- Pods - the official concept page for what a Pod is and how its containers relate.
- Configure Liveness, Readiness and Startup Probes - the definitive task guide for the three probe types.
- Resource Management for Pods and Containers - how requests and limits drive scheduling and enforcement.
- Init Containers - the ordered run-to-completion containers used in this module.
- Pod Lifecycle - Pod phases and container states like CrashLoopBackOff and OOMKilled.
Next
03-deployments-and-replicasets — wrap Pods in a controller that keeps the right number running and handles rolling updates for you.