EthicsComputerCourses
Join training +91 90532 10052

Pods

Kubernetes · Lesson 5 of 30 Intermediate 7 min read Updated

Kubernetes never runs a container on its own. It always wraps containers in a Pod. Every Deployment, Job and StatefulSet you will meet later is really a way of creating and managing Pods, so understanding them well makes the rest of the course much easier.

In this lesson you will write a Pod manifest, learn what the different statuses mean, build a Pod with an init container and a sidecar, and see how Kubernetes stops a Pod politely.

What a Pod is

A Pod is a group of one or more containers that always run together on the same node and share:

  • One network identity. The Pod has one IP address. Containers in it reach each other on localhost, and they cannot both listen on the same port.
  • Volumes. A volume defined in the Pod can be mounted into several of its containers, so they can share files.
  • A life. They are scheduled together, started together and deleted together.

Most Pods have exactly one container. Put several containers in one Pod only when they are tightly coupled helpers of the same app, for example a log shipper next to a web server. Two independent apps, such as a web app and its database, belong in separate Pods so each can be scaled and updated on its own.

A Pod manifest

YAML
apiVersion: v1
kind: Pod
metadata:
  name: menu
  labels:
    app: menu
spec:
  containers:
    - name: web
      image: nginx:1.30-alpine
      ports:
        - containerPort: 80
      env:
        - name: SHOP_CITY
          value: Lucknow
  • apiVersion: v1 and kind: Pod say what kind of object this is.
  • metadata.name must be unique among Pods in the namespace; labels are tags you will use to select Pods later.
  • spec.containers is a list. Each container needs a name and an image.
  • containerPort is mainly documentation; the container listens on whatever port its program opens.
  • env sets environment variables, just like docker run -e.

Save it as menu.yaml and run kubectl apply -f menu.yaml, which prints pod/menu created. You can override the image's command with command (replaces the Dockerfile ENTRYPOINT) and args (replaces CMD).

Phases and statuses

Every Pod has a phase, a simple summary of where it is in its life:

PhaseMeaning
PendingAccepted, but not all containers are running yet (waiting for a node or for images)
RunningBound to a node and at least one container is running
SucceededAll containers finished with exit code 0 and will not restart
FailedAll containers have stopped and at least one failed
UnknownThe node stopped reporting, so the state cannot be read

The STATUS column of kubectl get pods shows something more detailed. These are the ones you will see most:

STATUSWhat it usually means
ContainerCreatingImage is downloading or volumes are being attached
ErrImagePull, ImagePullBackOffWrong image name or tag, or a private registry without credentials
CrashLoopBackOffThe container keeps exiting; Kubernetes waits longer before each restart
OOMKilledThe container used more memory than its limit and was killed
CompletedThe container finished successfully (common for Jobs)
TerminatingThe Pod is shutting down

Restart policy

spec.restartPolicy decides what the kubelet does when a container exits:

  • Always (the default): restart it, whatever the exit code. Right for servers.
  • OnFailure: restart only after a non-zero exit code. Right for batch work.
  • Never: do not restart.

Restarts happen in place on the same node, with growing delays. A restart policy does not protect you from a node failing: a bare Pod on a dead node is simply lost. That is why you will run real apps through a Deployment or another controller, never as bare Pods.

Init containers and sidecars

Init containers run one after another, each to completion, before the main containers start. They are good for setup work: waiting for a database, downloading files or preparing a config.

A sidecar container is a helper that starts before the main containers and keeps running beside them for the Pod's whole life. Since Kubernetes 1.33 sidecars are a stable feature: you declare one as an init container with restartPolicy: Always.

This Pod uses both. The init container writes a web page into a shared emptyDir volume (an empty folder that lives as long as the Pod), Nginx serves it, and a sidecar prints Nginx's access log:

YAML
apiVersion: v1
kind: Pod
metadata:
  name: menu-board
spec:
  initContainers:
    - name: write-menu
      image: busybox:1.37
      command: ["sh", "-c", "echo '<h1>Today: masala chai Rs 20</h1>' > /work/index.html"]
      volumeMounts:
        - name: html
          mountPath: /work
    - name: log-reader
      image: busybox:1.37
      restartPolicy: Always
      command: ["sh", "-c", "tail -F /logs/access.log"]
      volumeMounts:
        - name: logs
          mountPath: /logs
  containers:
    - name: web
      image: nginx:1.30-alpine
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
        - name: logs
          mountPath: /var/log/nginx
  volumes:
    - name: html
      emptyDir: {}
    - name: logs
      emptyDir: {}

Apply it and check it twice, a few seconds apart:

Terminal
kubectl apply -f menu-board.yaml
kubectl get pod menu-board
kubectl get pod menu-board
Output
pod/menu-board created
NAME         READY   STATUS     RESTARTS   AGE
menu-board   0/2     Init:0/2   0          3s
NAME         READY   STATUS    RESTARTS   AGE
menu-board   2/2     Running   0          21s

Init:0/2 means none of the two init containers has finished starting yet. Later READY 2/2 counts the main container and the sidecar. Now send a request and read the sidecar's output:

Terminal
kubectl port-forward pod/menu-board 8080:80 &
curl -s http://localhost:8080
kubectl logs menu-board -c log-reader
Output
<h1>Today: masala chai Rs 20</h1>
127.0.0.1 - - [01/Nov/2026:10:15:32 +0000] "GET / HTTP/1.1" 200 34 "-" "curl/8.16.0" "-"

(The Forwarding from lines are left out; your date and curl version will differ.) The page came from the init container, and the log line reached the sidecar through the shared logs volume. -c chooses which container's log to read.

Stopping a Pod politely

When a Pod is deleted, the kubelet sends SIGTERM to each container's main process and waits for the grace period, 30 seconds by default (terminationGracePeriodSeconds). Anything still running after that gets SIGKILL. Well-written apps catch SIGTERM, finish the requests they are serving, close database connections and exit. A preStop hook can run a command before SIGTERM is sent, for example a short sleep so that load balancers stop sending traffic first.

Common mistakes

  • Running real apps as bare Pods. Nothing recreates them if their node dies. Use a Deployment.
  • Putting the app and its database in one Pod. They cannot scale or update separately. Use two Pods.
  • Expecting the Pod IP to stay the same. A replacement Pod gets a new IP. Use a Service to reach Pods.
  • Ignoring SIGTERM in your app. Users see cut-off requests on every deploy.

Key points

  • A Pod is one or more containers that share an IP address, volumes and a node; most Pods have one container.
  • Pods have a phase (Pending, Running, Succeeded, Failed, Unknown); the STATUS column gives more detail such as CrashLoopBackOff.
  • restartPolicy restarts containers in place but cannot save a Pod whose node has died.
  • Init containers run to completion first; sidecars are init containers with restartPolicy: Always and run for the whole Pod life.
  • Deleting a Pod sends SIGTERM, waits the grace period (30 seconds by default), then sends SIGKILL.

Exercise

Write a Pod called exam-results with one main container running nginx:1.30-alpine. Add an init container from busybox:1.37 that waits until the DNS name results-db can be resolved, checking every 2 seconds (the shell loop is until nslookup results-db; do sleep 2; done). What will kubectl get pod exam-results show while no such name exists?

Show solution
YAML
apiVersion: v1
kind: Pod
metadata:
  name: exam-results
spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.37
      command: ["sh", "-c", "until nslookup results-db; do sleep 2; done"]
  containers:
    - name: web
      image: nginx:1.30-alpine

While results-db cannot be resolved, the init container keeps looping, so the Pod shows READY 0/1 and STATUS Init:0/1, and Nginx never starts. As soon as a Service called results-db exists in the same namespace, the loop ends and the Pod becomes Running.

Quick Quiz

1. How do two containers in the same Pod talk to each other over the network?

2. Which restart policy suits a web server?

3. How do you declare a sidecar container in Kubernetes 1.33 and later?

4. What does a Pod status of ImagePullBackOff usually point to?

Learn Kubernetes with a mentor

Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call