Pods, the Smallest Unit You Deploy
Kubernetes never runs a container on its own. What a pod adds, why sidecars share one, and why you will almost never create one by hand.
Kubernetes never runs a container on its own. What a pod adds, why sidecars share one, and why you will almost never create one by hand.
You have a cluster and you know it runs containers. So the natural expectation is that "container" is the thing you create, and it is not. Kubernetes has no object called a container. The smallest thing you can ask for is a pod.
That indirection looks like bureaucracy until you see what it buys. By the end of this lesson you will know what a pod adds to a container, why some pods hold more than one, what happens when a pod dies — and why, despite spending a whole lesson on them, you will almost never create one by hand.
A pod is one or more containers that are always scheduled together, on one node, sharing a network address and able to share storage.
For the common case — one container — a pod is a thin wrapper, and the useful way to think of it is as the unit of scheduling. The scheduler places pods, not containers. Resources are requested per pod. A pod is on exactly one node, always.
Four things come with that, and all four are consequences of "on one node, together".
One IP address, belonging to the pod
Every container in it shares that address and its port space,
so they reach each other on localhost — the one place in
containers where localhost means more than one container.
Shared storage
Volumes are declared on the pod and can be mounted into several of its containers.
A shared lifecycle
The containers start together, live on the same node, and die together.
A shared fate for scheduling
If the node cannot fit the whole pod, none of it runs there.
Here is a pod described as YAML. Save it as pod.yaml:
Every Kubernetes object has the same four top-level fields, and it is worth naming them once:
apiVersion — which version of the API this object is written
against. Pods are core and long-stable, so plain v1. Deployments
live in a group, so theirs is apps/v1.
kind — the type of object.
metadata — its name, its namespace, and its labels. The name
identifies it; the labels are how everything else finds it, which
matters enormously from the Services lesson on.
spec — the desired state. Everything specific to the object
type is in here.
Apply it:
READY 1/1 means one container in the pod, one of them ready.
Four commands cover almost everything, and they map onto the Docker commands you already know.
describe is the one to build a habit around. Its last section is
the events — a timeline of what the cluster did with this pod,
in the cluster's own words:
Read that against the component lesson: the scheduler assigned it, then the kubelet on that node pulled and started it. When a pod misbehaves, this list usually contains the reason in plain English.
Note the -- in kubectl exec -it web -- sh. Everything after it is
the command for the container; everything before belongs to
kubectl. Omitting it makes kubectl try to interpret your
container's flags as its own.
Reach the pod from your machine the same way as before:
The multi-container pod is the reason pods exist, so it is worth one concrete example rather than a description.
A sidecar is a second container that supports the main one. Here the application writes logs to a file, and a sidecar reads that file and prints it to standard output where the cluster's log collection can see it:
The emptyDir volume is created empty when the pod starts, mounted
into both containers, and deleted when the pod goes away. Two
containers, one directory — which is only possible because they are
in one pod on one node.
With more than one container you have to say which you mean:
There is also the init container, which runs to completion before the main containers start. It is the right place for a one-off prerequisite — waiting for a database, fetching a config file, running a migration:
If an init container fails, the pod does not proceed — which is exactly the behaviour you want for a prerequisite.
Here is the limitation that motivates the next lesson. Delete the pod:
It is gone, and nothing brings it back. Compare that with a container crashing inside a pod:
Two failures that sound identical and are handled by completely different parts of the system.
The kubelet restarts it.
Same pod, same name, same IP address, RESTARTS goes up by
one. A pod's default restartPolicy is Always, and the
kubelet on that node does the work.
Nothing recreates it. Ever.
A pod has no controller watching it, so the object is simply gone.
And this happens routinely without you: a node drained for maintenance evicts its pods, a node out of memory kills them, a node that dies loses them.
A pod is never recreated. It has no controller watching it. This is why creating pods directly is something you do to learn, to experiment, and almost never in a system that has to stay up.
A pod moves through phases, and the STATUS column shows either a
phase or a more specific reason:
| Status | Meaning |
|---|---|
Pending | Accepted, not running yet — usually unscheduled |
ContainerCreating | Assigned; the kubelet is pulling or starting |
Running | At least one container is running |
Succeeded | All containers exited 0 and will not restart |
Failed | All containers exited, at least one with an error |
CrashLoopBackOff | Crashing repeatedly; restarts are being delayed |
ImagePullBackOff | The image could not be pulled |
Terminating | Being deleted; waiting on graceful shutdown |
CrashLoopBackOff is worth understanding rather than fearing. The
container keeps exiting and the kubelet keeps restarting it, backing
off exponentially — ten seconds, twenty, forty, up to five minutes —
so a broken pod does not consume the node. The status is a symptom;
kubectl logs --previous is where the cause is.
Deletion is graceful by default. Kubernetes sends SIGTERM, waits
for terminationGracePeriodSeconds — thirty by default — and then
sends SIGKILL. That grace period is your application's chance to
finish in-flight requests, which is only useful if your process
actually handles SIGTERM, which is only true if it is process 1.
You have met the unit and its limitation: a pod runs your container and nobody replaces it when it disappears. The Deployment is what fixes that, and it is the object you will actually use — but it is described in terms of a pod, so nothing here is wasted.
Before that, the next lesson steps back to the manifests themselves:
what kubectl apply really does, why declaring the end state beats
issuing commands, and how to keep these YAML files organised. Delete
your pod first and watch nothing happen — the absence is the point.
NAME READY STATUS RESTARTS AGE
web 1/1 Running 0 8sNAME READY STATUS IP NODE
web 1/1 Running 10.244.1.7 learning-workerEvents:
Type Reason Age Message
---- ------ ---- -------
Normal Scheduled 30s Successfully assigned default/web to
learning-worker
Normal Pulling 29s Pulling image "nginx:1.27-alpine"
Normal Pulled 27s Successfully pulled image
Normal Created 27s Created container nginx
Normal Started 27s Started container nginxNAME READY STATUS RESTARTS AGE
web 1/1 Running 1 2mapiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80kubectl apply -f pod.yaml
kubectl get podskubectl get pod web -o wide # node, IP, and status
kubectl describe pod web # everything, including events
kubectl logs web # its output
kubectl exec -it web -- sh # a shell inside itkubectl get pod web -o widekubectl port-forward pod/web 8080:80apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
volumes:
- name: logs
emptyDir: {}
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- 'while true; do echo "$(date) request served"
>> /var/log/app.log; sleep 5; done'
volumeMounts:
- name: logs
mountPath: /var/log
- name: log-shipper
image: busybox:1.36
command: ['sh', '-c', 'tail -F /var/log/app.log']
volumeMounts:
- name: logs
mountPath: /var/logkubectl apply -f sidecar.yaml
kubectl logs app-with-sidecar -c log-shipperkubectl logs app-with-sidecar -c app
kubectl exec -it app-with-sidecar -c log-shipper -- shspec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- 'until nc -z db 5432; do echo waiting; sleep 2; done'
containers:
- name: api
image: myapp:1.4.2kubectl delete pod web
kubectl get pods
# No resources found in default namespace.kubectl apply -f pod.yaml
kubectl exec web -- kill 1 # kill the main process
kubectl get pod web# The minimal pod, and the four fields every object has
apiVersion: v1 # which API version
kind: Pod # what type of object
metadata:
name: web # its identity
labels:
app: web # how other objects find it
spec: # the desired state
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80# Creating and inspecting
kubectl apply -f pod.yaml
kubectl get pods
kubectl get pod web -o wide # node and pod IP
kubectl get pod web -o yaml # the full object, as stored
kubectl describe pod web # everything, ending in events
kubectl logs web # output
kubectl logs web --previous # the crashed attempt's output
kubectl logs -f web # follow
kubectl exec -it web -- sh # shell inside ("--" matters)
kubectl port-forward pod/web 8080:80 # tunnel from your machine
kubectl delete pod web # gone, and not coming back
# Multi-container pods: name the container
kubectl logs app-with-sidecar -c log-shipper
kubectl exec -it app-with-sidecar -c app -- sh
# Statuses worth knowing on sight
# Pending not scheduled — describe says why
# ContainerCreating kubelet is pulling or starting it
# Running at least one container is up
# CrashLoopBackOff exiting repeatedly — logs --previous
# ImagePullBackOff bad image name, tag, or no credentials
# Terminating shutting down within the grace period
# The two kinds of failure
# container crashes -> kubelet restarts it, same pod, RESTARTS+1
# pod is deleted -> nothing recreates it. Use a Deployment.