In Kubernetes you never run a bare container. The unit you create, schedule and delete is the Pod. This chapter covers what a Pod is, how to describe one in YAML, and how to inspect and debug it.
What is a Pod?
A Pod is the smallest deployable unit in Kubernetes. It wraps one or more containers that are always scheduled together on the same compute node.
Containers inside the same Pod share:
- Network namespace: one IP address, and they reach each other on
localhost - Storage volumes: any volume declared on the Pod can be mounted by each container
- Lifecycle: they start, stop and are scheduled as a group
Containers in different Pods do not share these. They talk to each other through the Pod IP or a Service.
Pod (IP 10.244.1.7)
+-------------------------------------------+
| Container A <-- localhost --> Container B |
| \ / |
| +---- shared volume ------+ |
+-------------------------------------------+
One container per Pod (the usual case)
Most Pods hold a single application container. Think of the Pod as a thin wrapper that gives Kubernetes something to schedule.
Multi-container Pods
Use these only when containers are tightly coupled and must live and scale together. Common patterns:
- Sidecar: helper that extends the main container (log shipper, proxy)
- Ambassador: proxies outbound connections for the main container
- Adapter: transforms output of the main container into a standard format
Do not put a web frontend and its database in the same Pod. They scale differently and should have separate lifecycles.
Pods are ephemeral
A Pod is not self-healing. If the node dies, or you delete the Pod, it is gone and keeps the same name only if something else recreates it. Pods get a new IP when recreated. That is why in practice you do not create Pods directly but through higher level objects (Deployment, StatefulSet, Job), which are covered later.
Creating a Pod: Imperative
Recall from Chapter 2 that kubectl run issues a POST to the kube-apiserver and creates a Pod, not a container.
$ kubectl run nginx --image=nginx:1.27
pod/nginx created
Check it:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 15s
What the columns mean:
| Column | Meaning |
|---|---|
READY |
Ready containers / total containers in the Pod |
STATUS |
Pod phase or reason (Running, Pending, CrashLoopBackOff...) |
RESTARTS |
How many times the containers were restarted |
AGE |
Time since creation |
More detail:
$ kubectl get pods -o wide # adds node name and Pod IP
$ kubectl get pod nginx -o yaml # the full object stored in etcd
Creating a Pod: Declarative (YAML Manifest)
The preferred way. You describe the desired state in a file, commit it to Git, and ask Kubernetes to make it true.
# nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
env: dev
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
The four top-level fields
Every Kubernetes object manifest has these:
| Field | Purpose |
|---|---|
apiVersion |
Which API group and version defines this kind (v1 for core objects like Pod) |
kind |
The type of object (Pod, Deployment, Service ...) |
metadata |
Identity: name, namespace, labels, annotations |
spec |
The desired state, whose shape depends on the kind |
Once created, Kubernetes also adds a status field, which is the observed state, written by the system and not by you.
Applying it
$ kubectl apply -f nginx-pod.yaml
pod/nginx-pod created
apply is idempotent. Edit the file and run it again and Kubernetes works out the difference.
Some fields on a running Pod are immutable (e.g. the container
imagecan be changed, but most of the rest ofspeccannot). To change those you delete and recreate the Pod.
Generating YAML instead of writing it
Writing manifests from scratch is error prone. Let kubectl produce a starting point:
$ kubectl run nginx-pod --image=nginx:1.27 --dry-run=client -o yaml > nginx-pod.yaml
--dry-run=clientbuilds the object locally and does not send it-o yamlprints it as YAML
You can also ask the cluster what any field means:
$ kubectl explain pod.spec.containers
$ kubectl explain pod.spec.containers.resources
Imperative vs declarative
| Imperative | Declarative |
|---|---|
kubectl run, kubectl delete pod |
kubectl apply -f file.yaml |
| Quick experiments, one-offs | Repeatable, reviewable, version controlled |
| You tell Kubernetes what to do | You tell Kubernetes what you want |
Inspecting and Debugging Pods
describe
Shows the Pod's configuration plus a list of events. First place to look when a Pod is stuck in Pending or ImagePullBackOff.
$ kubectl describe pod nginx-pod
Events at the bottom tell the story, for example:
Scheduled default-scheduler Successfully assigned default/nginx-pod to minikube
Pulling kubelet Pulling image "nginx:1.27"
Pulled kubelet Successfully pulled image
Created kubelet Created container nginx
Started kubelet Started container nginx
This ties back to Chapter 2: kube-scheduler picks the node, then the kubelet on that node pulls the image and starts the container.
logs
$ kubectl logs nginx-pod
$ kubectl logs -f nginx-pod # follow
$ kubectl logs nginx-pod -c nginx # pick a container in a multi-container Pod
$ kubectl logs nginx-pod --previous # logs of the last crashed instance
exec
Run a command inside a running container:
$ kubectl exec nginx-pod -- ls /usr/share/nginx/html
$ kubectl exec -it nginx-pod -- /bin/bash # interactive shell
Note the -- which separates kubectl flags from the command to run.
port-forward
Pods get a cluster-internal IP. To reach one from your machine for testing:
$ kubectl port-forward pod/nginx-pod 8080:80
Then open http://localhost:8080. This forwards localhost:8080 to port 80 of the Pod and runs until you press Ctrl+C.
Common bad statuses
| Status | Usual cause |
|---|---|
Pending |
No node has enough resources, or a volume cannot be bound |
ContainerCreating |
Image being pulled, or volume mounting |
ImagePullBackOff / ErrImagePull |
Wrong image name/tag, or no registry credentials |
CrashLoopBackOff |
The container starts and exits repeatedly. Check logs --previous |
OOMKilled (in describe) |
Container exceeded its memory limit |
Pod Lifecycle
Pod phases
The status.phase field gives a high level summary:
| Phase | Meaning |
|---|---|
Pending |
Accepted by the cluster, but containers are not running yet |
Running |
Bound to a node and at least one container is running or starting |
Succeeded |
All containers exited with status 0 and will not restart |
Failed |
All containers have terminated and at least one failed |
Unknown |
The node cannot be reached, so state cannot be determined |
Restart policy
Controls what the kubelet does when a container exits:
spec:
restartPolicy: Always # Always (default) | OnFailure | Never
Always: for long running servicesOnFailure/Never: for run-to-completion tasks
Deleting a Pod
$ kubectl delete pod nginx-pod
$ kubectl delete -f nginx-pod.yaml
Kubernetes sends SIGTERM to the containers and waits up to the grace period (30 seconds by default) before sending SIGKILL. Your application should handle SIGTERM to shut down cleanly.
Labels, Selectors and Annotations
Labels
Key-value pairs in metadata.labels used to organise and select objects. They are how Services find Pods and how Deployments manage them.
$ kubectl get pods --show-labels
$ kubectl get pods -l app=nginx
$ kubectl get pods -l 'env in (dev,staging)'
$ kubectl label pod nginx-pod tier=frontend
Annotations
Also key-value, but for non-identifying metadata (build info, links, tool configuration). They cannot be used in selectors.
metadata:
annotations:
description: "Demo web server"
Namespaces
Namespaces split one physical cluster into virtual clusters. Names only need to be unique within a namespace.
Default namespaces:
| Namespace | Purpose |
|---|---|
default |
Where objects go if you do not specify one |
kube-system |
Kubernetes system components |
kube-public |
Readable by all users, rarely used |
kube-node-lease |
Node heartbeat objects |
$ kubectl get namespaces
$ kubectl create namespace dev
$ kubectl apply -f nginx-pod.yaml -n dev
$ kubectl get pods -n dev
$ kubectl get pods -A # all namespaces
Or set it in the manifest:
metadata:
name: nginx-pod
namespace: dev
Deleting a namespace deletes everything inside it.
Resource Requests and Limits
The kube-scheduler uses requests to decide which node a Pod fits on. The kubelet enforces limits at runtime.
spec:
containers:
- name: nginx
image: nginx:1.27
resources:
requests:
cpu: "250m" # 0.25 of a core
memory: "64Mi"
limits:
cpu: "500m"
memory: "128Mi"
- CPU is compressible: a container over its limit is throttled
- Memory is not: a container over its limit is killed (
OOMKilled) mmeans millicores, so1000mequals 1 CPU
Health Probes
Probes let the kubelet check whether your application is actually healthy, not just running.
| Probe | Question it answers | If it fails |
|---|---|---|
livenessProbe |
Is the container still alive? | Container is restarted |
readinessProbe |
Is it ready to receive traffic? | Removed from Service endpoints |
startupProbe |
Has the slow-starting app finished booting? | Container is restarted; disables the other probes until it passes |
spec:
containers:
- name: nginx
image: nginx:1.27
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
Probe mechanisms: httpGet, tcpSocket, exec (run a command), and grpc.
Init Containers and Multi-container Example
Init containers run to completion, one after another, before the app containers start. Useful for waiting on a dependency or preparing files.
apiVersion: v1
kind: Pod
metadata:
name: web-with-sidecar
spec:
volumes:
- name: shared-data
emptyDir: {}
initContainers:
- name: init-content
image: busybox:1.36
command: ["sh", "-c", "echo 'Hello from init' > /data/index.html"]
volumeMounts:
- name: shared-data
mountPath: /data
containers:
- name: nginx
image: nginx:1.27
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
- name: log-tail
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
What happens:
1. init-content writes a file to the shared emptyDir volume and exits
2. Only then do nginx and log-tail start
3. Both app containers share the same Pod IP and the shared-data volume
Target a specific container with -c:
$ kubectl logs web-with-sidecar -c nginx
$ kubectl exec -it web-with-sidecar -c log-tail -- sh
Summary
- A Pod is the smallest deployable unit: one or more containers sharing network and volumes.
- Pods are ephemeral and not self-healing, so you normally manage them through higher level controllers.
- Prefer declarative manifests applied with
kubectl apply -f; use--dry-run=client -o yamlto generate them. - Debug with
describe(events),logs,execandport-forward. - Labels select objects, namespaces isolate them, requests/limits drive scheduling and enforcement, and probes tell the
kubeletabout health.