On this page
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
- What a Pod is, what its containers share and why Kubernetes uses Pods instead of bare containers
- How Pod networking works with the Container Network Interface (CNI), and how to design Pods that are stateless and easy to replace
- How to write Pod manifests and add probes, resource requests and limits, and volumes step by step
- The
kubectlcommands to create, inspect, debug and delete Pods - How to reach a running Pod with
kubectl port-forwardandkubectl 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:
- Every container in a Pod gets the same IP address.
- Containers in the same Pod talk to each other over
localhost. - 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.
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 resource | What it means |
|---|---|
| Network namespace | All 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 namespace | All containers see the same hostname. |
| IPC namespace | Containers can communicate with System V IPC or POSIX message queues. |
| Volumes | Containers 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?"
- No: put them in the same Pod. Example: a web server and a helper that syncs its local files.
- Yes: use separate Pods. Example: an API and a database already talk over the network and need to scale independently.
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.
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:
- 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.
- 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:
| Problem | Why it happens |
|---|---|
| Poor data durability | Database data stored in the Pod is lost when the Pod is replaced. |
| Lower availability | The database restarts every time the application crashes and the Pod is recreated. |
| Weaker stability | A 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:
Create it and list your Pods:
Expected output:
Delete it when you are done:
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:
Build the image and push it to a registry your cluster can pull from:
Then write the Pod manifest. Save it as weather-api-pod.yaml:
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:
Expected output:
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
| Probe | Question it answers | What happens on failure |
|---|---|---|
| Liveness | Is the application still working? | The container is restarted. |
| Readiness | Is the application ready to accept traffic? | The Pod is removed from Service endpoints. It is not restarted. |
| Startup | Has 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:
- httpGet: sends an HTTP request to a path and port. Success is a status code from 200 to 399.
- tcpSocket: opens a TCP connection to a port. Success is an established connection.
- exec: runs a command inside the container. Success is exit code 0.
- 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:
Then add the probes to the container in the manifest, at the same level as image:
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:
- Request: the amount reserved for the container. The scheduler uses it to find a node with enough free capacity.
- 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:
| Resource | Over the limit | Result |
|---|---|---|
| Memory | The container is killed with OOMKilled | It is restarted according to the Pod's restart policy |
| CPU | The container is throttled (slowed down) | It keeps running, just slower |
The units you will see most often:
| Resource | Examples | Meaning |
|---|---|---|
| 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:
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 type | When to use it | Survives Pod deletion? |
|---|---|---|
emptyDir | Temporary storage, shared between the containers of one Pod | No |
hostPath | A directory from the node's own disk (testing only) | Yes, but only on that node |
persistentVolumeClaim | Cloud disks and network storage for production workloads | Yes |
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:
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 manifest | kubectl apply -f weather-api-pod.yaml |
List Pods (add -o wide for IP and node) | kubectl get pods |
| See details, probe results and events | kubectl describe pod weather-api |
| Read the logs | kubectl logs weather-api |
| Run a command inside the container | kubectl exec weather-api -- <command> |
| Reach the Pod from my machine | kubectl port-forward weather-api 8080:8080 |
| Delete the Pod | kubectl delete pod weather-api |
Create a Pod imperatively
For a quick test you can create a Pod without a manifest:
This is also handy for throwaway debugging Pods, such as a BusyBox Pod that stays alive for one hour:
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:
List running Pods
Expected output:
Add -o wide to see each Pod's IP address and the node it runs on:
Expected output:
Inspect Pod details
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
Run commands inside the container
Run a one-off command:
Expected output:
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.
Port-forward to reach the API locally
Reach the Pod from your machine without a Service or an Ingress:
Then, in another terminal:
Copy files from the container
kubectl cp copies files in either direction. It needs the tar binary inside the container.
Check resource usage
Expected output:
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
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:
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
Step 3 — Watch the Pod come up
Expected output:
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
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
Expected output:
The values should stay within your limits (750m CPU, 512Mi memory).
Step 6 — Port-forward and call the API
In a new terminal:
Expected output:
Step 7 — Verify the volume mount
Expected output:
The emptyDir volume works. Files in it survive container restarts, but not deletion of the Pod.
Step 8 — Check the environment variables
Expected output:
Step 9 — View the logs
Step 10 — Clean up
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:
Step 2 — Apply both Pods
The Weather API manifest is weather-api-pod.yaml from earlier in this lesson:
Step 3 — Note the Pod IP addresses
Expected output:
Step 4 — Call the API from inside the BusyBox Pod
Open a shell in the BusyBox container:
Inside the shell, call the Weather API by its Pod IP. Replace 10.244.0.6 with the IP from the previous step:
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
Common problems
| What you see | Likely cause | Where to look |
|---|---|---|
Pending for a long time | No node has enough free CPU or memory for the requests | Events in kubectl describe pod |
ImagePullBackOff or ErrImagePull | Wrong image name or tag, or the registry needs credentials | kubectl describe pod |
CrashLoopBackOff | The app exits on start, or a liveness probe keeps failing | kubectl logs <pod> --previous |
OOMKilled in the last state | The container used more memory than its limit | kubectl describe pod, then raise the limit or fix the leak |
READY 0/1 while Running | The readiness probe is failing | Unhealthy 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.
- 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. - 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.
- Design Pods to be self-contained and stateless. Data that must survive belongs in a database, a cache or a
PersistentVolume. - Startup probes protect slow starters, liveness probes restart stuck containers, and readiness probes keep unready Pods out of Service traffic.
- Requests reserve CPU and memory, limits cap them. Over the memory limit means
OOMKilled; over the CPU limit means throttling. Set both in production. emptyDirsurvives container restarts but not Pod deletion; use apersistentVolumeClaimfor data that must outlive the Pod.- Your debugging toolkit:
kubectl describe,kubectl logs,kubectl exec,kubectl port-forwardandkubectl 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.