Table of contents
- Why Pods, of All Things
- Inside a Pod
- Running the Simplest Pod
- Pod Lifecycle
- Health Checks: Liveness, Readiness, Startup
- Multi-Container Pattern: Sidecar
- Init Container: Containers That Run First and Finish
- Resource Requests and Limits
- Pods Are Disposable
Why Pods, of All Things
When you first touch Kubernetes, you start with kubectl run. You see a message like pod/nginx created. You were trying to start a container — so why does it say “pod”?
In Kubernetes, the unit of deployment is not the container but the Pod. A pod is a unit wrapping one or more containers, and containers within the same pod share networking and storage. In other words, containers belonging to the same pod use the same IP and can reach each other via localhost.
Why add this extra layer? At first, it seems like containers alone would suffice as the deployment unit. But in real operations, you frequently encounter container pairs that must travel closely together, like “main process + log collector” or “main process + TLS proxy.” They need to share the same network and storage, and it’s natural for them to be bundled as a deployment unit. Pods are the mechanism for grouping “inseparable pairs.”
Inside a Pod
There’s an interesting supporting character inside a pod: the Pause container. Before the app containers declared by the user come up, a very small container called Pause starts first. Its role is to hold on to the network namespace. The reason network information persists even when app containers restart is thanks to Pause.
flowchart TB
subgraph POD["Pod (IP: 10.244.1.5)"]
PAUSE[Pause Container<br/>Network Namespace Holder]
APP[App Container]
SIDE[Sidecar Container]
VOL[(Shared Volume)]
APP -.-|localhost| SIDE
APP --> VOL
SIDE --> VOL
end
Thanks to this structure, containers within a pod share:
- Network: Same IP, same port space. Communication via localhost
- Storage: Pod-level volumes mounted by multiple containers
- Lifecycle: They operate together when the pod starts and dies
Each pod gets a unique IP, but when a pod dies, that IP is gone. A newly created pod receives a different IP. This instability is why we abstract pods behind Services instead of pointing to them directly (covered in Part 5).
Running the Simplest Pod
Let’s start by creating a YAML for a pod with a single nginx container.
# nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
Apply this file to the cluster and check the status:
kubectl apply -f nginx-pod.yaml
kubectl get pod nginx -o wide
When the STATUS changes to Running, the container is up and running. You can also exec into the pod and call itself with curl:
kubectl exec -it nginx -- curl localhost:80
If you see the “Welcome to nginx” page, it’s a success. However, in practice, directly creating pods like this is rare. A standalone pod has no self-healing — if it dies, it simply disappears. That’s why we use controllers like Deployment, covered in Part 4, to manage pods.
Pod Lifecycle
Knowing the stages a pod goes through from birth to death is a huge help when debugging.
- Pending: The pod has been accepted by the cluster, but containers haven’t started yet. It may be downloading images or selecting a node for placement
- Running: The pod has been assigned to a node and at least one container is running
- Succeeded: All containers have terminated successfully and won’t restart (commonly seen in one-off workloads like Jobs)
- Failed: A container terminated abnormally and won’t restart
- Unknown: Communication with the node is lost, so the status can’t be determined
stateDiagram-v2
[*] --> Pending
Pending --> Running: Container started
Pending --> Failed: Image pull failure
Running --> Succeeded: Normal termination (RestartPolicy=Never)
Running --> Failed: Abnormal termination (cannot restart)
Running --> Running: Container restart
Running --> [*]: Deleted
Succeeded --> [*]: Deleted
Failed --> [*]: Deleted
Running kubectl describe pod <name> shows lifecycle transitions in chronological order in the Events section. Familiar error messages like “ImagePullBackOff” and “CrashLoopBackOff” appear here.
Health Checks: Liveness, Readiness, Startup
Just because a pod is Running doesn’t necessarily mean it’s working correctly. The container might be alive but the application could be stuck in a deadlock, or it might still be booting and not ready to receive traffic. Kubernetes uses three probes to distinguish these states.
Let’s look at the flow of when each probe acts and what decisions it makes:
flowchart TB
START["Container Start"] --> STP{"startupProbe exists?"}
STP -->|Yes| SLOOP{"startupProbe passed?"}
SLOOP -->|"Failed (below failureThreshold)"| SLOOP
SLOOP -->|Passed| READY["liveness/readiness activated"]
SLOOP -->|"Threshold exceeded"| KILL["Restart container"]
STP -->|No| READY
READY --> LP{"livenessProbe failed?"}
LP -->|Yes| KILL
LP -->|No| RP{"readinessProbe failed?"}
RP -->|Yes| REMOVE["Remove from Service Endpoints"]
RP -->|No| SERVE["Normal traffic reception"]
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: myapp:1.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10
Here’s what each one does:
- Liveness: “Is this container alive?” If it fails, kubelet restarts the container. Primarily used for deadlock detection
- Readiness: “Is this container ready to receive traffic?” If it fails, the pod is removed from the Service (connects to Part 5). Essential for apps with long boot times
- Startup: “Has this container finished booting?” Liveness and readiness checks are deferred until this succeeds. Designed for legacy apps with very long startup times
If readiness isn’t configured properly, a newly started pod receives traffic before it’s ready and starts throwing 500 errors. This is a common cause of deployment failures.
Multi-Container Pattern: Sidecar
Putting multiple containers in a single pod is for special cases. The most common pattern is the Sidecar. It’s a structure where a helper container is attached alongside the main container.
Common sidecar use cases include:
- Log collection: Reads log files written by the main app and ships them to a centralized logging system
- Proxy: Intercepts all traffic like Istio’s Envoy to add encryption and observability
- Config reloader: Detects ConfigMap changes and sends a SIGHUP signal to the main app
Let’s look at a sidecar example that shares file-based logs:
apiVersion: v1
kind: Pod
metadata:
name: app-with-logger
spec:
volumes:
- name: logs
emptyDir: {}
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-forwarder
image: fluent/fluent-bit:latest
volumeMounts:
- name: logs
mountPath: /var/log/app
readOnly: true
Both containers mount the emptyDir volume together. The main app writes logs to /var/log/app, and the log-forwarding sidecar reads from the same path and ships them externally. Since they’re in the same pod, communication happens purely through the filesystem with no network hops.
Init Container: Containers That Run First and Finish
When there are initialization tasks that must complete before the main container starts, use Init Containers. Init containers run sequentially, and only after all of them succeed does the main container start.
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db 5432; do echo "waiting db"; sleep 2; done']
- name: run-migration
image: myapp:1.0
command: ['./migrate.sh']
containers:
- name: app
image: myapp:1.0
This pod operates in the following order:
wait-for-dbwaits until the DB becomes accessiblerun-migrationruns the DB schema migration- The main container
appstarts
Visualized on a timeline, it looks like this. The key is sequential execution where each stage must succeed before moving to the next:
sequenceDiagram
participant K as kubelet
participant I1 as initContainer:wait-for-db
participant I2 as initContainer:run-migration
participant M as container:app
K->>I1: Run
I1->>I1: Poll DB connection
I1-->>K: exit 0 (success)
K->>I2: Run
I2->>I2: Migration
I2-->>K: exit 0 (success)
K->>M: Run (main)
Note over M: Pod enters Running state
You could put this logic inside the main app instead of using init containers. But separating it makes responsibilities clear and keeps images small. Most importantly, if the migration fails, the main app never starts — preventing the accident of serving traffic in an invalid state.
Resource Requests and Limits
When running a pod, it’s good practice to specify “how much CPU this pod uses and what the maximum memory should be.” This is configured with resources.requests and resources.limits.
spec:
containers:
- name: app
image: myapp:1.0
resources:
requests:
cpu: "200m" # 0.2 CPU cores
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
requests is the value the Scheduler references when placing a pod. It sets the criterion: “this node must have at least 200m of free CPU for this pod to fit.” limits is the runtime ceiling. If the memory limit is exceeded, the container gets OOMKilled.
This setting is the most common source of production incidents. If requests are too large, resources are wasted; too small, and pods contend with each other. If limits are too tight, the app falls into a restart loop from OOMKilled. You need to measure actual load and adjust accordingly.
Pods Are Disposable
To close this part, here’s the most important perspective: pods are ephemeral. When they die and come back, they’re different pods with different IPs, and local disk contents are gone.
This characteristic isn’t a flaw — it’s intentional design. The premise that pods can easily die and easily be reborn is what makes self-healing, horizontal scaling, and rolling updates possible. If your application can’t accept this premise (e.g., it stores important state on local disk), you won’t be able to fully benefit from Kubernetes.
That’s why the Kubernetes-friendly approach is to push state to external systems (databases, object storage, persistent volumes) and design pods as stateless processing units.
In the next part, we’ll look at how to manage multiple pods declaratively through controllers like Deployments, rather than managing pods directly. We’ll also cover how rolling updates and rollbacks work safely.




Loading comments...