Understanding Kubernetes Pods: The Complete Guide

Lesson 6 of 39 19 min read Updated

A Pod is the smallest thing you can deploy in Kubernetes. It is not a container. It is a thin wrapper around one or more containers that share the same network, the same storage and the same lifecycle.

Think of a Pod as a shared apartment. Each room is a container with its own space (CPU and memory), but everyone uses the same front door (IP address), the same mailbox (hostname) and the same common areas (volumes).

You never create containers directly on a cluster. Every container runs inside a Pod, so understanding Pods is the first step to working confidently with Kubernetes. In this lesson you will go from a one-line NGINX Pod to a .NET API with health checks, resource limits and a volume.

What you will learn

  1. What a Pod is, what its containers share and why Kubernetes uses Pods instead of bare containers
  2. How Pod networking works with the Container Network Interface (CNI), and how to design Pods that are stateless and easy to replace
  3. How to write Pod manifests and add probes, resource requests and limits, and volumes step by step
  4. The kubectl commands to create, inspect, debug and delete Pods
  5. How to reach a running Pod with kubectl port-forward and kubectl exec, and from another Pod

What is a Pod?

Kubernetes never runs a container on its own. It always wraps containers in a Pod. That has three direct consequences:

  1. Every container in a Pod gets the same IP address.
  2. Containers in the same Pod talk to each other over localhost.
  3. They can share files through volumes.

The golden rule of Pods is simple: all containers in a Pod always run on the same node. A Pod can never be split across worker nodes.

A Pod is scheduled as one unit. Kubernetes picks a node for the whole Pod, starts all of its containers there and removes them together. The Pod, not the container, is the lowest level you work with through the Kubernetes API.

Why Pods instead of bare containers?

Imagine a web API and a log collector that reads the API's log files from disk. They need the same filesystem, so putting them on two machines would break the log collector. A Pod groups them and guarantees they always land on the same node.

At the same time, they stay separate containers, so each one keeps its own resource limits. If the log collector leaks memory, the kernel kills only that container, not your web API.

Real applications are rarely a single process. A classic WordPress site needs an NGINX web server and a PHP-FPM interpreter running side by side. On a virtual machine you would install both on the same host. With containers the rule is one process per container, so you end up with two containers that must talk to each other and share files.

Wiring that up by hand with raw Docker commands (custom networks, volume mounts, several machines) quickly becomes unmanageable. Pods solve exactly this problem. Note also that Kubernetes only manages containers it started through a Pod: a container you start manually on a node is invisible to it.

What containers in a Pod share

Shared resourceWhat it means
Network namespaceAll containers share one IP address and one port space. They reach each other on localhost, so two containers cannot both listen on the same port.
UTS namespaceAll containers see the same hostname.
IPC namespaceContainers can communicate with System V IPC or POSIX message queues.
VolumesContainers can mount the same volume, even at different paths, to share files.

Containers in different Pods are fully isolated: different IP addresses and different hostnames, even when they run on the same physical node.

Deciding what goes in one Pod

Ask yourself one question: "Will these containers still work correctly if they land on different machines?"

  1. No: put them in the same Pod. Example: a web server and a helper that syncs its local files.
  2. Yes: use separate Pods. Example: an API and a database already talk over the network and need to scale independently.
Do not put your app and its database in one Pod. It is a common beginner mistake. They communicate over the network anyway, you need to scale them separately, and you will see below how it hurts durability, availability and stability.

Single-container and multi-container Pods

Most Pods you create will hold one container. That is the standard pattern for microservices: one Pod, one container, one process.

Multi-container Pods appear when two processes are so tightly coupled that they must run together, for example an application container plus a logging sidecar that streams its log files.

Pod networking

When Kubernetes schedules a Pod onto a node, it gives the Pod its own private IP address. Every Pod can reach every other Pod by that IP, whichever node they run on. This is called the flat network model: no NAT and no port mapping between Pods.

The piece that implements this model is the Container Network Interface (CNI), a standard interface between the container runtime and the network. Popular CNI plugins are Flannel, Calico and Cilium. The runtime calls the plugin, which hands out the Pod's IP address and sets up connectivity across nodes.

Pod IPs are not stable. A Pod gets a new IP every time it is recreated, so calling Pods by IP is fine for testing but not for real traffic. A later lesson introduces the Service, which gives a stable DNS name and virtual IP in front of a changing set of Pods.

How to design your Pods

The second golden rule: a Pod must be easy to destroy and recreate at any moment. A node failure, a deployment update or an eviction under resource pressure can end a Pod without warning, and your application must cope with that. Two principles help:

  1. A Pod should be self-contained. It includes everything the application needs to start. If the Pod is recreated from scratch, the app comes back with no manual steps.
  2. A Pod should be stateless. Any data that must survive a restart lives outside the Pod: in a database, an external cache or a Kubernetes PersistentVolume.

Packing the application and its database into the same Pod causes three problems:

ProblemWhy it happens
Poor data durabilityDatabase data stored in the Pod is lost when the Pod is replaced.
Lower availabilityThe database restarts every time the application crashes and the Pod is recreated.
Weaker stabilityA memory leak in the application can push the whole Pod and the node under pressure and take the database down too.

Run the database in its own Pod instead. Pods can reach each other by IP or DNS name, so splitting them does not break connectivity.

Writing Pod manifests

You describe a Pod in a manifest: a YAML file that states the desired state. You send it to the Kubernetes API server, and the scheduler places the Pod on a healthy node. This is declarative configuration: you say what you want, not how to get there.

The simplest Pod

A single container running NGINX. Save it as simple-web.yaml:

apiVersion: v1
kind: Pod
metadata:
name: simple-web
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"

Create it and list your Pods:

kubectl apply -f simple-web.yaml
kubectl get pods

Expected output:

NAME READY STATUS RESTARTS AGE
simple-web 1/1 Running 0 12s

Delete it when you are done:

kubectl delete pod simple-web

A .NET API in a Pod

Now a real application: an ASP.NET Core Weather API. First you need a container image. Save this as Dockerfile in the project root:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "WeatherApi.dll"]

Build the image and push it to a registry your cluster can pull from:

docker build -t myregistry/weather-api:1.0 .
docker push myregistry/weather-api:1.0

Then write the Pod manifest. Save it as weather-api-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
name: weather-api
labels:
app: weather-api
spec:
containers:
- name: weather-api
image: myregistry/weather-api:1.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ASPNETCORE_ENVIRONMENT
value: "Development"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"

The labels block tags the Pod with app: weather-api. Labels are how other Kubernetes objects find Pods, and they get their own lesson later in this section. Apply the manifest and check the Pod:

kubectl apply -f weather-api-pod.yaml
kubectl get pods weather-api

Expected output:

NAME READY STATUS RESTARTS AGE
weather-api 1/1 Running 0 18s

Since .NET 8 the official ASP.NET Core images already listen on port 8080, so ASPNETCORE_URLS is optional here. Setting it makes the port explicit in the manifest.

Health checks with probes

Kubernetes restarts a container automatically when its main process crashes. But what if the process is still running and simply stuck or deadlocked? That is what probes are for.

The three probe types

ProbeQuestion it answersWhat happens on failure
LivenessIs the application still working?The container is restarted.
ReadinessIs the application ready to accept traffic?The Pod is removed from Service endpoints. It is not restarted.
StartupHas the application finished starting?Liveness and readiness probes wait until it succeeds. If it keeps failing, the container is restarted.

How a probe checks health

Each probe uses one of these mechanisms:

  1. httpGet: sends an HTTP request to a path and port. Success is a status code from 200 to 399.
  2. tcpSocket: opens a TCP connection to a port. Success is an established connection.
  3. exec: runs a command inside the container. Success is exit code 0.
  4. grpc: calls the standard gRPC health check service, for gRPC apps.

Adding probes to the .NET Pod

ASP.NET Core has built-in health check middleware. These lines in Program.cs expose /healthz for liveness and /ready for readiness:

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/healthz");
app.MapHealthChecks("/ready");

Then add the probes to the container in the manifest, at the same level as image:

spec:
containers:
- name: weather-api
image: myregistry/weather-api:1.0
# ports, env and resources as before
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 2
startupProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 10

The startup probe gives the app about 50 seconds to start (10 failures × 5 seconds, after a 3-second delay). Until it succeeds, Kubernetes does not run the liveness or readiness probes.

Resource requests and limits

Every container can declare how much CPU and memory it needs, with two settings:

  1. Request: the amount reserved for the container. The scheduler uses it to find a node with enough free capacity.
  2. Limit: the most the container may use. The kernel enforces this cap.

What happens when a container goes over its limit depends on the resource:

ResourceOver the limitResult
MemoryThe container is killed with OOMKilledIt is restarted according to the Pod's restart policy
CPUThe container is throttled (slowed down)It keeps running, just slower

The units you will see most often:

ResourceExamplesMeaning
CPU"250m", "1"250 millicores is 0.25 of a CPU core; "1" is one full core
Memory"256Mi", "1Gi"256 mebibytes; 1 gibibyte (power-of-two units)

Here is the resources block for the Weather API container:

spec:
containers:
- name: weather-api
image: myregistry/weather-api:1.0
resources:
requests:
cpu: "250m"
memory: "128Mi"
limits:
cpu: "750m"
memory: "512Mi"

This reserves at least 0.25 CPU and 128 MiB of memory for the container and caps it at 0.75 CPU and 512 MiB.

Volumes

Files written inside a container are ephemeral: they disappear when the container restarts. A volume attaches storage that outlives a single container.

Volume typeWhen to use itSurvives Pod deletion?
emptyDirTemporary storage, shared between the containers of one PodNo
hostPathA directory from the node's own disk (testing only)Yes, but only on that node
persistentVolumeClaimCloud disks and network storage for production workloadsYes

A volume has two parts. You declare it once under the Pod's spec.volumes, then mount it into each container that needs it with volumeMounts. Here is an emptyDir for log files:

spec:
volumes:
- name: log-storage
emptyDir: {}
containers:
- name: weather-api
image: myregistry/weather-api:1.0
volumeMounts:
- name: log-storage
mountPath: /app/logs

Everyday Pod commands

These are the commands you will use to work with Pods. All examples use the weather-api Pod.

I want to…Command
Create a Pod from a manifestkubectl apply -f weather-api-pod.yaml
List Pods (add -o wide for IP and node)kubectl get pods
See details, probe results and eventskubectl describe pod weather-api
Read the logskubectl logs weather-api
Run a command inside the containerkubectl exec weather-api -- <command>
Reach the Pod from my machinekubectl port-forward weather-api 8080:8080
Delete the Podkubectl delete pod weather-api

Create a Pod imperatively

For a quick test you can create a Pod without a manifest:

kubectl run weather-api --image=myregistry/weather-api:1.0 --port=8080

This is also handy for throwaway debugging Pods, such as a BusyBox Pod that stays alive for one hour:

kubectl run busybox-test --image=busybox:1.37 --restart=Never -- sleep 3600

kubectl run always creates a single bare Pod. --restart=Never sets the Pod's restart policy so the container is not started again when sleep ends, and everything after -- replaces the container's command.

Create a Pod from a YAML manifest

This is the recommended, declarative way:

kubectl apply -f weather-api-pod.yaml

List running Pods

kubectl get pods

Expected output:

NAME READY STATUS RESTARTS AGE
weather-api 1/1 Running 0 45s

Add -o wide to see each Pod's IP address and the node it runs on:

kubectl get pods -o wide

Expected output:

NAME READY STATUS RESTARTS AGE IP NODE
weather-api 1/1 Running 0 52s 10.244.1.12 worker-01

Inspect Pod details

kubectl describe pod weather-api

This shows labels, the node, container state, probe settings, resource requests and limits, and the event history. It is the first command to run when something looks wrong.

View Pod logs

# Print the logs
kubectl logs weather-api

# Stream new log lines in real time
kubectl logs weather-api -f

# Logs of the previous container instance, after a crash
kubectl logs weather-api --previous

Run commands inside the container

Run a one-off command:

kubectl exec weather-api -- printenv ASPNETCORE_ENVIRONMENT

Expected output:

Development

Open an interactive shell. The Debian-based ASP.NET Core image has bash; for minimal images use sh, and note that chiseled images ship without any shell.

kubectl exec -it weather-api -- /bin/bash

Port-forward to reach the API locally

Reach the Pod from your machine without a Service or an Ingress:

kubectl port-forward weather-api 8080:8080

Then, in another terminal:

curl http://localhost:8080/weatherforecast

Copy files from the container

kubectl cp copies files in either direction. It needs the tar binary inside the container.

kubectl cp weather-api:/app/logs/app.log ./local-app.log

Check resource usage

kubectl top pod weather-api

Expected output:

NAME CPU(cores) MEMORY(bytes)
weather-api 15m 87Mi
kubectl top needs metrics-server. If you get error: Metrics API not available, the cluster has no metrics-server installed. On minikube, run minikube addons enable metrics-server.

Delete a Pod

# By name
kubectl delete pod weather-api

# Or with the manifest that created it
kubectl delete -f weather-api-pod.yaml

A deleted Pod first enters the Terminating state. It gets a grace period, 30 seconds by default, to finish active requests before it is killed. The next lesson walks through this shutdown sequence in detail.

Hands-on: a complete Weather API Pod

Now put everything together: one manifest for the .NET Weather API with probes, resource limits and a volume. Then deploy it and verify each feature.

Step 1 — Write the complete manifest

Save this as weather-api-full.yaml:

apiVersion: v1
kind: Pod
metadata:
name: weather-api
labels:
app: weather-api
version: "1.0"
spec:
volumes:
- name: log-storage
emptyDir: {}
containers:
- name: weather-api
image: myregistry/weather-api:1.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: Logging__LogFilePath
value: "/app/logs/weather-api.log"
resources:
requests:
cpu: "250m"
memory: "128Mi"
limits:
cpu: "750m"
memory: "512Mi"
volumeMounts:
- name: log-storage
mountPath: /app/logs
startupProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 2

Logging__LogFilePath is an app setting read by the API's own file logger. The double underscore maps to the Logging:LogFilePath configuration key in .NET.

Step 2 — Apply the manifest

kubectl apply -f weather-api-full.yaml

Step 3 — Watch the Pod come up

kubectl get pods --watch

Expected output:

NAME READY STATUS RESTARTS AGE
weather-api 0/1 Pending 0 0s
weather-api 0/1 ContainerCreating 0 1s
weather-api 0/1 Running 0 3s
weather-api 1/1 Running 0 8s

READY switches from 0/1 to 1/1 once the startup and readiness probes pass. Press Ctrl+C to stop watching.

Step 4 — Verify the probes

kubectl describe pod weather-api

Scroll to the Events section at the bottom. You should see the Pod being scheduled, the image being pulled and the container being created and started. No Unhealthy events means the probes are passing.

Step 5 — Check resource usage

kubectl top pod weather-api

Expected output:

NAME CPU(cores) MEMORY(bytes)
weather-api 12m 92Mi

The values should stay within your limits (750m CPU, 512Mi memory).

Step 6 — Port-forward and call the API

kubectl port-forward weather-api 8080:8080

In a new terminal:

curl http://localhost:8080/weatherforecast

Expected output:

[{"date":"2026-03-06","temperatureC":25,"summary":"Warm"},
{"date":"2026-03-07","temperatureC":18,"summary":"Cool"}]

Step 7 — Verify the volume mount

kubectl exec weather-api -- ls /app/logs

Expected output:

weather-api.log

The emptyDir volume works. Files in it survive container restarts, but not deletion of the Pod.

Step 8 — Check the environment variables

kubectl exec weather-api -- printenv | grep ASPNETCORE

Expected output:

ASPNETCORE_URLS=http://+:8080
ASPNETCORE_ENVIRONMENT=Production

Step 9 — View the logs

kubectl logs weather-api

Step 10 — Clean up

kubectl delete -f weather-api-full.yaml
You built a production-shaped Pod. Start with a basic container, then add probes, then resource limits, then volumes. Building the manifest in layers like this makes each new setting easy to test and easy to debug.

Hands-on: Pod-to-Pod communication

Now see the flat network model in action. You will run a small BusyBox Pod next to the Weather API Pod and call the API from inside the cluster by its Pod IP.

Step 1 — Create a BusyBox test Pod

Save this as busybox-test-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
name: busybox-test
labels:
app: busybox-test
purpose: testing
spec:
containers:
- name: busybox
image: busybox:1.37
command: ["sleep", "3600"]
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "100m"
memory: "64Mi"
restartPolicy: Never

Step 2 — Apply both Pods

The Weather API manifest is weather-api-pod.yaml from earlier in this lesson:

kubectl apply -f busybox-test-pod.yaml
kubectl apply -f weather-api-pod.yaml

Step 3 — Note the Pod IP addresses

kubectl get pods -o wide

Expected output:

NAME READY STATUS RESTARTS AGE IP NODE
busybox-test 1/1 Running 0 30s 10.244.0.5 node01
weather-api 1/1 Running 0 25s 10.244.0.6 node01

Step 4 — Call the API from inside the BusyBox Pod

Open a shell in the BusyBox container:

kubectl exec -it busybox-test -- sh

Inside the shell, call the Weather API by its Pod IP. Replace 10.244.0.6 with the IP from the previous step:

wget -qO- http://10.244.0.6:8080/weatherforecast
exit

The BusyBox Pod reaches the Weather API Pod directly by IP. No firewall rules and no port mappings are needed. That is the flat network model at work.

Step 5 — Clean up

kubectl delete pod weather-api busybox-test

Common problems

What you seeLikely causeWhere to look
Pending for a long timeNo node has enough free CPU or memory for the requestsEvents in kubectl describe pod
ImagePullBackOff or ErrImagePullWrong image name or tag, or the registry needs credentialskubectl describe pod
CrashLoopBackOffThe app exits on start, or a liveness probe keeps failingkubectl logs <pod> --previous
OOMKilled in the last stateThe container used more memory than its limitkubectl describe pod, then raise the limit or fix the leak
READY 0/1 while RunningThe readiness probe is failingUnhealthy events in kubectl describe pod

Summary

A Pod is the smallest deployable unit in Kubernetes: one or more containers that share a network, storage and lifecycle, and always run on the same node. You describe it in a YAML manifest and grow that manifest step by step with probes, resources and volumes.

  1. Containers in one Pod share an IP address and hostname and talk over localhost. If containers must run on the same machine, put them in one Pod; otherwise use separate Pods.
  2. Every Pod gets its own IP, and every Pod can reach every other Pod directly. A CNI plugin such as Flannel, Calico or Cilium implements this flat network.
  3. Design Pods to be self-contained and stateless. Data that must survive belongs in a database, a cache or a PersistentVolume.
  4. Startup probes protect slow starters, liveness probes restart stuck containers, and readiness probes keep unready Pods out of Service traffic.
  5. Requests reserve CPU and memory, limits cap them. Over the memory limit means OOMKilled; over the CPU limit means throttling. Set both in production.
  6. emptyDir survives container restarts but not Pod deletion; use a persistentVolumeClaim for data that must outlive the Pod.
  7. Your debugging toolkit: kubectl describe, kubectl logs, kubectl exec, kubectl port-forward and kubectl top. A deleted Pod gets a 30-second grace period by default.

Next, you will follow a Pod through its lifecycle, from Pending to a graceful shutdown.