| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64 |
| Applies to | Any Linux host with Podman installed |
| Privilege | Rootful examples on the lab host; flags behave the same rootless unless noted |
| Scope | Pod concept, infra containers, default shared namespaces, adding members with --pod, localhost communication, --share syntax including +pid, exit policy, lifecycle commands, inspect, stats, removal, and pod vs container vs user-defined network decisions. Does not cover pod port publishing, pod DNS depth, Quadlet .pod units, or podman kube play. |
| Related guides | List containers with podman ps |
A Podman pod groups containers that need shared namespaces and joint lifecycle. If two workloads should talk over localhost inside one network stack, a pod is usually the right tool. If they should reach each other by DNS name on separate IPs, put them on a user-defined network instead.
This guide creates pods on Podman 5.8.2, adds member containers, proves what is actually shared, and walks lifecycle commands end to end.
What is a Podman pod?
A pod is a cgroup parent and namespace bundle for multiple containers:
Pod
├── infra container
├── application container A
└── application container B
Shared by default:
├── network namespace
├── IPC namespace
└── UTS namespacePID namespace is not shared by default. The CLI default for --share is:
ipc,net,utsDo not assume every namespace is shared — that misread causes confusion when processes or hostnames behave differently than expected.
Why does Podman use an infra container?
The infra container is a lightweight process that keeps the pod shared namespaces alive while application containers start, stop, or restart.
podman pod create enables it by default:
--infra=trueOn the lab host the infra container runs catatonit as PID 1. It appears in podman ps with a name like PODID-infra and no conventional OCI image tag in the list output.
The infra container starts when the pod starts and normally remains until the pod is removed. Application containers join the namespaces the infra container holds.
Modern Podman does not need k8s.gcr.io/pause
Older tutorials pulled k8s.gcr.io/pause for pod infra containers. Podman 5.x on this host does not require that image for normal pod creation.
Create an empty pod:
podman pod create --name podman-pod-demoPodman prints the new pod ID:
04f8cb6509d22b70b425c10cfdfe2f8245efc4b723a2436b06d9c90f78a77c52List pods:
podman pod psSample output:
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
04f8cb6509d2 podman-pod-demo Created Less than a second ago 9e0522e0355d 1The # OF CONTAINERS count includes the infra container. INFRA ID points at that holder even before you add application containers.
Override the built-in infra image only when you have a deliberate reason:
--infra-image=IMAGEFor most workflows, accept the default local infra container.
Create your first Podman pod
podman pod create allocates the pod object and infra container. You can explicitly start the empty pod first:
podman pod start podman-pod-demoThis is useful for demonstrating the pod lifecycle, but it is not required before podman run --pod; starting a member can start the required infra-container dependency automatically.
A created-but-not-started pod shows Created in podman pod ps; after podman pod start the status becomes Running.
Inspect namespace sharing:
podman pod inspect podman-pod-demo --format '{{json .SharedNamespaces}}'Sample output:
["ipc","net","uts"]That JSON array is the authoritative shared-namespace list for this pod.
Useful podman pod inspect fields beyond namespaces:
State— pod lifecycle stateInfraContainerID— infra container IDExitPolicy—continueorstopCgroupParent— cgroup path for the pod group
Port publishing and pod-level DNS belong in the dedicated pod networking guide — this article focuses on namespace sharing and lifecycle.
Add containers to a pod
Attach members with --pod on Run containers with podman run. Each member inherits the pod shared namespaces; do not attach a separate --network to pod members.
Start a web server in the pod:
podman run -d --pod podman-pod-demo --name app-one registry.access.redhat.com/ubi9/httpd-24Add a second container for testing:
podman run -d --pod podman-pod-demo --name app-two registry.access.redhat.com/ubi9/ubi-minimal sleep 3600List containers with pod columns:
podman ps --pod --filter pod=podman-pod-demoSample output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES POD ID PODNAME
9e0522e0355d 3 minutes ago Up 3 minutes 04f8cb6509d2-infra 04f8cb6509d2 podman-pod-demo
ce7c2da4fdfb registry.access.redhat.com/ubi9/httpd-24:latest 1 minute ago Up 1 minute app-one 04f8cb6509d2 podman-pod-demo
d7e5b44cb202 registry.access.redhat.com/ubi9/ubi-minimal:latest sleep 3600 1 minute ago Up 1 minute app-two 04f8cb6509d2 podman-pod-demoThe infra row has no image column value; application rows show their images. Both app containers reference the same POD ID and PODNAME.
Show member names from the pod listing:
podman pod ps --ctr-namesSample output:
POD ID NAME STATUS CREATED INFRA ID NAMES
04f8cb6509d2 podman-pod-demo Running 5 minutes 9e0522e0355d 04f8cb6509d2-infra,app-one,app-twoThe NAMES column lists infra plus every application container in join order.
Communicate between pod containers over localhost
Because pod members share a network namespace, 127.0.0.1 inside one container is the same loopback interface as in every other member.
The UBI httpd-24 image listens on port 8080. From app-two, request that port:
podman exec app-two curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/Sample output:
403HTTP 403 still proves the TCP connection succeeded — app-two reached app-one over shared localhost. A connection failure would print 000 and a curl error.
Fetch the response body to confirm the web server answered:
podman exec app-two curl -s http://127.0.0.1:8080/ | head -3Sample output:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">Two ordinary containers on a user-defined network would use DNS names such as database:5432, not 127.0.0.1, because each has its own network namespace. Pods trade that separation for shared localhost.
What else is shared by default?
| Namespace | Shared by default? | Effect |
|---|---|---|
| Network | Yes | same interfaces, IPs, and localhost |
| IPC | Yes | shared /dev/shm, semaphores, message queues |
| UTS | Yes | shared hostname |
| PID | No | separate process trees unless you add pid to --share |
| cgroup namespace | No | separate per container; pod cgroup parent is a related grouping concept |
UTS namespace
Both members report the pod name as hostname:
podman exec app-one cat /etc/hostnameSample output:
podman-pod-demoCheck the second member:
podman exec app-two cat /etc/hostnameSample output:
podman-pod-demoSame hostname confirms UTS sharing.
IPC namespace
Compare the IPC namespace identifier from each container:
podman exec app-one readlink /proc/self/ns/ipcSample output:
ipc:[4026532736]Ask app-two for the same identifier:
podman exec app-two readlink /proc/self/ns/ipcSample output:
ipc:[4026532736]Matching ipc:[...] identifiers confirm that both containers are in the same IPC namespace.
PID namespace
Without PID sharing, one member does not see the other application processes in /proc. From app-two, list process comm names:
podman exec app-two sh -c 'for p in /proc/[0-9]*; do cat $p/comm 2>/dev/null; done' | sort -uSample output:
sh
sleephttpd from app-one does not appear — PID namespaces remain separate on the default pod.
Change shared namespaces with --share
Default sharing is ipc,net,uts. To append PID while keeping defaults, use the + prefix:
podman pod create --share=+pid --name shared-pid-podInspect the result:
podman pod inspect shared-pid-pod --format '{{json .SharedNamespaces}}'Sample output:
["ipc","net","pid","uts"]+pid appended pid to the default set.
This syntax trap catches many administrators:
--share=+pid → append PID to ipc,net,uts
--share=pid → replace defaults with only PIDCreate a pod with PID only:
podman pod create --share=pid --name pid-only-podInspect it:
podman pod inspect pid-only-pod --format '{{json .SharedNamespaces}}'Sample output:
["pid"]--share=pid does not mean “defaults plus PID.” It means “share only PID,” which removes network and IPC sharing. Use +pid when you want localhost communication and shared process visibility.
Create a pod without shared namespaces
An empty share list disables namespace sharing:
podman pod create --share="" --name no-share-podInspect namespaces and infra:
podman pod inspect no-share-pod --format 'Share: {{json .SharedNamespaces}} Infra: {{.InfraContainerID}}'Sample output:
Share: [] Infra:With no namespaces to share, Podman does not create an infra container. At that point the pod mainly groups lifecycle and cgroup parentage rather than providing Kubernetes-style colocation. Most production sidecar patterns need at least network sharing.
Create a pod without an infra container
Disable the infra container explicitly:
podman pod create --infra=false --name no-infra-podInspect the pod:
podman pod inspect no-infra-pod --format 'Share: {{json .SharedNamespaces}} Infra: {{.InfraContainerID}}'Sample output:
Share: [] Infra:Without an infra holder, shared namespace lifecycle behaves differently and several pod workflows become constrained. Keep --infra=true as the default teaching path.
Pod lifecycle commands
Pod-level commands operate on every member container together. The examples below use podman-pod-demo from earlier sections.
List pods
Print every pod on the host:
podman pod psShows pod ID, name, status, infra ID, and container count for each pod.
Stop a pod
Stop every container in the pod together:
podman pod stop podman-pod-demoSample output:
podman-pod-demoAfter stop, podman pod ps reports Exited for the pod and its containers.
Start a pod
Bring a stopped pod back to Running:
podman pod start podman-pod-demoBrings the infra container and stopped members back to Running.
Restart a pod
Cycle stop and start in one command:
podman pod restart podman-pod-demoStops and starts all pod containers in one command. Long-running sleep containers may need SIGKILL after the stop timeout, which Podman logs as a warning.
Pause and unpause
Freeze every process in the pod:
podman pod pause podman-pod-demoSample output:
04f8cb6509d22b70b425c10cfdfe2f8245efc4b723a2436b06d9c90f78a77c52Status becomes Paused. Resume with:
podman pod unpause podman-pod-demopodman pod kill sends signals for forced teardown; use it when graceful stop is not enough.
Inspect a Podman pod
Full configuration and state live in inspect output:
podman pod inspect podman-pod-demoPull selected fields without reading the entire JSON document:
podman pod inspect podman-pod-demo --format 'State: {{.State}} ExitPolicy: {{.ExitPolicy}}'Sample output:
State: Running ExitPolicy: continueUse inspect when you need container IDs, cgroup parent, hostname, network settings, or the exact SharedNamespaces list recorded at creation time.
Monitor pod resource usage with podman pod stats
Aggregate and per-container CPU and memory for a pod:
podman pod stats podman-pod-demo --no-streamSample output:
POD CID NAME CPU % MEM USAGE/ LIMIT MEM % NET IO BLOCK IO PIDS
04f8cb6509d2 9e0522e0355d 04f8cb6509d2-infra 0.00% 213kB / 8.052GB 0.00% 6.733kB / 978B -- / -- 1
04f8cb6509d2 d7e5b44cb202 app-two 0.10% 438.3kB / 8.052GB 0.01% 6.733kB / 978B -- / -- 1
04f8cb6509d2 ce7c2da4fdfb app-one 1.80% 19.82MB / 8.052GB 0.25% 6.733kB / 978B 8.192kB / 53.25kB 181Each member container gets its own row under the same POD ID. --no-stream prints one snapshot suitable for documentation. CPU and memory limit configuration is a separate topic.
Pod exit policy
podman pod create defaults to:
--exit-policy=continuecontinue
When the last regular application container exits, the infra container keeps running and the pod stays active. The demo pod with httpd and sleep members remained Running after unrelated short-lived containers exited elsewhere.
stop
When the last application container exits, the entire pod stops including the infra container.
Create a pod with stop policy:
podman pod create --exit-policy=stop --name exit-stop-podStart it and add a short-lived member:
podman pod start exit-stop-podAdd a container that exits after five seconds:
podman run -d --pod exit-stop-pod --name exit-app registry.access.redhat.com/ubi9/ubi-minimal sleep 5After five seconds, check pod status:
podman pod ps --filter name=exit-stop-pod --format '{{.Name}} {{.Status}}'Sample output:
exit-stop-pod ExitedBoth exit-app and the infra container exit when the last application container finishes.
podman kube play creates pods with stop behavior. Quadlet .pod units carry their own defaults in the Quadlet article — do not assume CLI and Quadlet exit policies match.
Pod restart policy
Set a default restart policy for containers joining the pod:
podman pod create --restart=on-failure --name restart-demoThe pod-level --restart value becomes the default applied to containers created inside that pod unless overridden per podman run. Detailed container restart behavior lives in Start, stop, and restart containers.
Remove a Podman pod
Remove a stopped pod:
podman pod rm exit-stop-podRemoving a running pod without force fails:
podman pod rm podman-pod-demoSample output (truncated):
Error: not all containers could be removed from pod 04f8cb6509d2...
Error: cannot remove container ... as it is running - running or paused containers cannot be removed without force: container state improperForce removal stops and deletes all members plus the infra container:
podman pod rm -f podman-pod-demopodman pod rm -f removes the pod object and member containers. Named volumes you created independently are not deleted. Anonymous volumes tied to removed containers follow normal Podman volume cleanup rules.
Podman pod vs container
| Topic | Container | Pod |
|---|---|---|
| Unit | one container object | group of containers |
| Network | own namespace by default | members share pod network by default |
| localhost | private to each container | shared across members |
| Lifecycle | podman start / stop per container |
podman pod commands manage the group |
| Best fit | independent services | tightly coupled sidecar-style workloads |
Not every multi-container application needs a pod. Two microservices that call each other by DNS name on a bridge network are usually separate containers, not pod members.
Pod vs user-defined network
Choose a pod when
- containers need the same network namespace
- workloads communicate over
localhost - published ports belong to the group as a unit
- shared IPC or UTS matters
- the layout resembles a Kubernetes pod or sidecar pair
- joint start/stop/restart is desirable
Choose separate containers on a user-defined network when
- each container should have its own IP and network namespace
- Aardvark DNS names are preferable to localhost
- multiple services need to bind the same port number internally
- lifecycle coupling should stay loose
- the architecture looks like normal service-to-service calls
Example patterns:
Pod:
app ↔ log-shipper via 127.0.0.1:8080
User-defined network:
frontend → api:8080 → database:5432See Podman networking modes for bridge, pasta, and DNS behavior on ordinary containers.
Podman pods vs Kubernetes pods
Similarities:
- shared network namespace concept
- multiple containers grouped for local communication
- sidecar and helper container patterns
- Kubernetes YAML can be rehearsed locally with Podman
Differences:
- Podman manages containers on one Linux host
- Kubernetes schedules pods across a cluster control plane
- Podman pod CLI flags do not map one-to-one to every Kubernetes pod spec field
podman kube playis the path for YAML import, notpodman pod createalone
Treat Podman pods as a local grouping primitive that resembles Kubernetes ergonomics, not as a miniature Kubernetes API.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
podman pod rm fails on running pod |
Members still running | podman pod stop first, or podman pod rm -f |
curl 127.0.0.1 fails between members |
Containers not in same pod, or service not listening | Confirm --pod on both; verify port inside the serving container |
--share=pid broke localhost |
Replaced defaults instead of appending | Recreate with --share=+pid |
| No infra container | --share="" or --infra=false |
Recreate with defaults unless intentional |
| Exit policy surprise | continue keeps infra alive |
Use --exit-policy=stop when pod should end with last app |
References
- podman-pod-create(1) — Podman documentation
- podman-pod(1) — Podman documentation
- podman-run(1) —
--podflag
Summary
A Podman pod groups containers under shared ipc, net, and uts namespaces by default, with an infra container keeping those namespaces alive. You create the pod with podman pod create, then attach members through podman run --pod; starting a member can start the required infra container automatically, though you may start the pod explicitly first when demonstrating lifecycle. On the lab host, app-two reached app-one over 127.0.0.1:8080 because both containers shared one network namespace — something two ordinary bridge-network containers cannot do.
PID namespace sharing is off unless you add it with --share=+pid. Writing --share=pid alone replaces the default list and removes network sharing, which is a common misconfiguration. Exit policy continue keeps the infra container running after the last app exits; stop tears down the whole pod.
Use pods for sidecar-style localhost coupling. Use separate containers on a user-defined network when each service needs its own IP and DNS name. Port publishing, pod DNS, and Quadlet .pod files are covered in sibling guides.

