Skip to main content
Kubernetes · lesson 2 of 10
Kubernetes beginner Lesson 2 of 10

Pods and Workloads

Understand pods, containers, labels, selectors, and how Kubernetes schedules and manages workload lifecycles.

By SkillByExample · Updated

Pod Basics

A Pod groups one or more containers together so they can run as a single unit on one node. Every container in a Pod shares the same network namespace (IP + ports) and can share storage volumes.

┌─────────────────────────────────────┐
│  Pod  (shared IP: 10.244.0.5)       │
│                                     │
│  ┌─────────────┐  ┌──────────────┐  │
│  │  app:nginx  │  │  sidecar:log │  │
│  │  port 80    │  │  (read logs) │  │
│  └─────────────┘  └──────────────┘  │
│                                     │
│  shared volume: /var/log/nginx      │
└─────────────────────────────────────┘

Your First Pod Manifest

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-nginx
  labels:
    app: my-nginx
    env: dev
spec:
  containers:
    - name: nginx
      image: nginx:1.25
      ports:
        - containerPort: 80
      resources:
        requests:
          cpu: "100m"
          memory: "64Mi"
        limits:
          cpu: "200m"
          memory: "128Mi"
# Apply the pod manifest
kubectl apply -f pod.yaml

# Check it started
kubectl get pods

# Detailed view — events, container status, node assignment
kubectl describe pod my-nginx

# Stream logs
kubectl logs -f my-nginx

# Open an interactive shell
kubectl exec -it my-nginx -- bash

# Delete the pod
kubectl delete pod my-nginx
# or
kubectl delete -f pod.yaml

Multi-Container Pod (Sidecar Pattern)

# pod-sidecar.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-with-sidecar
spec:
  volumes:
    - name: shared-logs
      emptyDir: {}

  containers:
    - name: app
      image: nginx:1.25
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/nginx

    - name: log-shipper
      image: busybox:1.36
      command: ["sh", "-c", "tail -f /logs/access.log"]
      volumeMounts:
        - name: shared-logs
          mountPath: /logs

Both containers communicate over localhost since they share the same network namespace.

Labels and Selectors

Labels are the primary way Kubernetes objects find and relate to each other.

# Applying labels to a pod
metadata:
  name: api-server
  labels:
    app: api          # what app
    version: v2       # which version
    env: production   # which environment
    tier: backend     # which tier
# List pods with their labels
kubectl get pods --show-labels

# Filter pods by label
kubectl get pods -l app=api
kubectl get pods -l env=production,tier=backend

# Add a label to a running pod (imperative)
kubectl label pod my-nginx region=us-east

# Remove a label
kubectl label pod my-nginx region-

# Select pods NOT matching a label
kubectl get pods -l 'env!=dev'

Pod Lifecycle

Pending → Running → Succeeded / Failed
              ↕
           CrashLoopBackOff (if container keeps failing)
# Watch pod status transitions in real time
kubectl get pods --watch

# Check why a pod is in CrashLoopBackOff
kubectl describe pod <name>   # look at Events section
kubectl logs <name> --previous  # logs from previous (crashed) run

Init Containers

Init containers run to completion before main containers start — useful for waiting on databases or seeding config.

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 postgres-service 5432; do echo waiting; sleep 2; done"

  containers:
    - name: app
      image: my-api:1.0
      ports:
        - containerPort: 8080

Resource Requests and Limits

resources:
  requests:
    cpu: "250m"      # 0.25 cores guaranteed
    memory: "128Mi"  # 128 MB guaranteed
  limits:
    cpu: "500m"      # max 0.5 cores
    memory: "256Mi"  # max 256 MB (OOMKilled if exceeded)
# Check resource usage for pods
kubectl top pods

# Check resource usage for nodes
kubectl top nodes

Health Probes

spec:
  containers:
    - name: api
      image: my-api:1.0
      livenessProbe:         # restart if this fails
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 10

      readinessProbe:        # only send traffic when this passes
        httpGet:
          path: /ready
          port: 8080
        initialDelaySeconds: 3
        periodSeconds: 5
ProbeFailure action
livenessProbeRestart the container
readinessProbeRemove from Service endpoints (no traffic)
startupProbeBlock liveness/readiness until startup completes

Learning Outcomes

You should be able to:

  • Write and apply a Pod manifest with kubectl apply
  • Explain the sidecar pattern and when to use it
  • Use labels and selectors to organize and filter resources
  • Configure resource requests/limits and health probes
  • Reason about why Services route to the right Pods

Practice

  1. Apply pod.yaml and pod-sidecar.yaml, add region=us-east to my-nginx, then select by that label. Before you run it, predict which Pods -l 'env!=dev' returns.
Solution
kubectl apply -f pod.yaml
kubectl apply -f pod-sidecar.yaml
kubectl wait --for=condition=Ready pod/my-nginx pod/app-with-sidecar --timeout=180s
kubectl label pod my-nginx region=us-east
kubectl get pods -l region=us-east --show-labels
kubectl get pods -l 'env!=dev'
$ kubectl apply -f pod.yaml
pod/my-nginx created
$ kubectl apply -f pod-sidecar.yaml
pod/app-with-sidecar created
$ kubectl wait --for=condition=Ready pod/my-nginx pod/app-with-sidecar --timeout=180s
pod/my-nginx condition met
pod/app-with-sidecar condition met
$ kubectl label pod my-nginx region=us-east
pod/my-nginx labeled
$ kubectl get pods -l region=us-east --show-labels
NAME       READY   STATUS    RESTARTS   AGE   LABELS
my-nginx   1/1     Running   0          8s    app=my-nginx,env=dev,region=us-east
$ kubectl get pods -l 'env!=dev'
NAME               READY   STATUS    RESTARTS   AGE
app-with-sidecar   2/2     Running   0          9s

app-with-sidecar has no env label at all, and != also matches Pods that lack the key. If you mean “has env, but not dev”, write -l 'env,env!=dev'.

  1. Prove the two containers in app-with-sidecar share a network namespace: from the log-shipper container, fetch nginx’s page over localhost. Then check which container’s logs record the request.
Solution
kubectl exec app-with-sidecar -c log-shipper -- wget -qO- http://localhost | grep '<title>'
kubectl logs app-with-sidecar -c log-shipper
kubectl logs app-with-sidecar -c app --tail=1
$ kubectl exec app-with-sidecar -c log-shipper -- wget -qO- http://localhost | grep '<title>'
<title>Welcome to nginx!</title>
$ kubectl logs app-with-sidecar -c log-shipper
127.0.0.1 - - [11/Oct/2026:05:05:24 +0000] "GET / HTTP/1.1" 200 615 "-" "Wget" "-"
$ kubectl logs app-with-sidecar -c app --tail=1
/docker-entrypoint.sh: Configuration complete; ready for start up

busybox has no web server, yet localhost:80 answered, because both containers share one IP. The request appears only in the sidecar’s logs. Mounting the volume over /var/log/nginx hides the image’s link from access.log to stdout, so nginx writes a real file and tail -f reads it.

  1. Write a Pod called memory-hog that runs tail /dev/zero in busybox:1.36 with a 64Mi memory limit. What status does it reach, and where do you find the reason?
Solution
# pod-oom.yaml
apiVersion: v1
kind: Pod
metadata:
  name: memory-hog
spec:
  containers:
    - name: hog
      image: busybox:1.36
      command: ["sh", "-c", "tail /dev/zero"]   # buffers forever, never finds a newline
      resources:
        requests:
          memory: "32Mi"
        limits:
          memory: "64Mi"
$ kubectl apply -f pod-oom.yaml
pod/memory-hog created
$ kubectl get pod memory-hog
NAME         READY   STATUS      RESTARTS      AGE
memory-hog   0/1     OOMKilled   3 (28s ago)   45s
$ kubectl describe pod memory-hog | grep -A 5 'Last State'
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Sun, 11 Oct 2026 10:36:08 +0530
      Finished:     Sun, 11 Oct 2026 10:36:08 +0530
    Ready:          False

tail keeps buffering zeros while it looks for a final line, so it hits 64Mi within a second and the kernel kills it. Exit code 137 is 128 + 9 (SIGKILL). The kubelet keeps restarting it, waiting longer before each attempt, which is why RESTARTS sits at 3 while ”28s ago” keeps growing. That time counts from the previous kill, and the next restart waits 40s.

  1. Give a plain nginx:1.25 Pod a livenessProbe on /healthz port 80, every 5 seconds. nginx has no such page. What happens over the next minute?
Solution
# pod-liveness.yaml
apiVersion: v1
kind: Pod
metadata:
  name: bad-liveness
spec:
  containers:
    - name: nginx
      image: nginx:1.25
      livenessProbe:
        httpGet:
          path: /healthz     # nginx has no such page: 404
          port: 80
        initialDelaySeconds: 5
        periodSeconds: 5
$ kubectl apply -f pod-liveness.yaml
pod/bad-liveness created
$ kubectl get pod bad-liveness
NAME           READY   STATUS    RESTARTS     AGE
bad-liveness   1/1     Running   2 (5s ago)   46s
$ kubectl describe pod bad-liveness | grep -E 'Liveness|Unhealthy|Killing'
    Liveness:       http-get http://:80/healthz delay=5s timeout=1s period=5s #success=1 #failure=3
  Warning  Unhealthy  6s (x6 over 36s)  kubelet            spec.containers{nginx}: Liveness probe failed: HTTP probe failed with statuscode: 404
  Normal   Killing    6s (x2 over 26s)  kubelet            spec.containers{nginx}: Container nginx failed liveness probe, will be restarted

The Pod says Running, but nginx is restarted about every 20 seconds (x2 over 26s, about 20 seconds apart): the 5-second initialDelaySeconds applies again after each restart, then three failed probes (#failure=3) 5 seconds apart. Any status outside 200-399 counts as a failure. A wrong liveness path turns a healthy app into a restart loop, which is why describe is the first place to look.

Clean up with kubectl delete pod my-nginx app-with-sidecar memory-hog bad-liveness.

Frequently Asked Questions

What exactly is a Pod in Kubernetes?
A Pod is the smallest deployable unit in Kubernetes. It usually contains one container, but can contain multiple containers that share networking and storage.
Why do we use labels and selectors?
Labels tag objects with key/value metadata. Selectors let controllers (like Deployments and Services) find the Pods they should manage or route traffic to.