Skip to content

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 image can be changed, but most of the rest of spec cannot). 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=client builds the object locally and does not send it
  • -o yaml prints 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 services
  • OnFailure / 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)
  • m means millicores, so 1000m equals 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 yaml to generate them.
  • Debug with describe (events), logs, exec and port-forward.
  • Labels select objects, namespaces isolate them, requests/limits drive scheduling and enforcement, and probes tell the kubelet about health.