Podman Pods: Run Multiple Containers Together

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:

text
Pod
 ├── infra container
 ├── application container A
 └── application container B

Shared by default:
 ├── network namespace
 ├── IPC namespace
 └── UTS namespace

PID namespace is not shared by default. The CLI default for --share is:

text
ipc,net,uts

Do 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:

text
--infra=true

On 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:

bash
podman pod create --name podman-pod-demo

Podman prints the new pod ID:

output
04f8cb6509d22b70b425c10cfdfe2f8245efc4b723a2436b06d9c90f78a77c52

List pods:

bash
podman pod ps

Sample output:

output
POD ID        NAME             STATUS   CREATED                 INFRA ID      # OF CONTAINERS
04f8cb6509d2  podman-pod-demo  Created  Less than a second ago  9e0522e0355d  1

The # 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:

text
--infra-image=IMAGE

For 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:

bash
podman pod start podman-pod-demo

This 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:

bash
podman pod inspect podman-pod-demo --format '{{json .SharedNamespaces}}'

Sample output:

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 state
  • InfraContainerID — infra container ID
  • ExitPolicycontinue or stop
  • CgroupParent — 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:

bash
podman run -d --pod podman-pod-demo --name app-one registry.access.redhat.com/ubi9/httpd-24

Add a second container for testing:

bash
podman run -d --pod podman-pod-demo --name app-two registry.access.redhat.com/ubi9/ubi-minimal sleep 3600

List containers with pod columns:

bash
podman ps --pod --filter pod=podman-pod-demo

Sample output:

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-demo

The 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:

bash
podman pod ps --ctr-names

Sample output:

output
POD ID        NAME             STATUS   CREATED      INFRA ID      NAMES
04f8cb6509d2  podman-pod-demo  Running  5 minutes    9e0522e0355d  04f8cb6509d2-infra,app-one,app-two

The 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:

bash
podman exec app-two curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/

Sample output:

output
403

HTTP 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:

bash
podman exec app-two curl -s http://127.0.0.1:8080/ | head -3

Sample output:

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:

bash
podman exec app-one cat /etc/hostname

Sample output:

output
podman-pod-demo

Check the second member:

bash
podman exec app-two cat /etc/hostname

Sample output:

output
podman-pod-demo

Same hostname confirms UTS sharing.

IPC namespace

Compare the IPC namespace identifier from each container:

bash
podman exec app-one readlink /proc/self/ns/ipc

Sample output:

output
ipc:[4026532736]

Ask app-two for the same identifier:

bash
podman exec app-two readlink /proc/self/ns/ipc

Sample output:

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:

bash
podman exec app-two sh -c 'for p in /proc/[0-9]*; do cat $p/comm 2>/dev/null; done' | sort -u

Sample output:

output
sh
sleep

httpd 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:

bash
podman pod create --share=+pid --name shared-pid-pod

Inspect the result:

bash
podman pod inspect shared-pid-pod --format '{{json .SharedNamespaces}}'

Sample output:

output
["ipc","net","pid","uts"]

+pid appended pid to the default set.

This syntax trap catches many administrators:

text
--share=+pid   → append PID to ipc,net,uts

--share=pid    → replace defaults with only PID

Create a pod with PID only:

bash
podman pod create --share=pid --name pid-only-pod

Inspect it:

bash
podman pod inspect pid-only-pod --format '{{json .SharedNamespaces}}'

Sample output:

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:

bash
podman pod create --share="" --name no-share-pod

Inspect namespaces and infra:

bash
podman pod inspect no-share-pod --format 'Share: {{json .SharedNamespaces}} Infra: {{.InfraContainerID}}'

Sample output:

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:

bash
podman pod create --infra=false --name no-infra-pod

Inspect the pod:

bash
podman pod inspect no-infra-pod --format 'Share: {{json .SharedNamespaces}} Infra: {{.InfraContainerID}}'

Sample output:

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:

bash
podman pod ps

Shows pod ID, name, status, infra ID, and container count for each pod.

Stop a pod

Stop every container in the pod together:

bash
podman pod stop podman-pod-demo

Sample output:

output
podman-pod-demo

After stop, podman pod ps reports Exited for the pod and its containers.

Start a pod

Bring a stopped pod back to Running:

bash
podman pod start podman-pod-demo

Brings the infra container and stopped members back to Running.

Restart a pod

Cycle stop and start in one command:

bash
podman pod restart podman-pod-demo

Stops 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:

bash
podman pod pause podman-pod-demo

Sample output:

output
04f8cb6509d22b70b425c10cfdfe2f8245efc4b723a2436b06d9c90f78a77c52

Status becomes Paused. Resume with:

bash
podman pod unpause podman-pod-demo

podman 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:

bash
podman pod inspect podman-pod-demo

Pull selected fields without reading the entire JSON document:

bash
podman pod inspect podman-pod-demo --format 'State: {{.State}} ExitPolicy: {{.ExitPolicy}}'

Sample output:

output
State: Running ExitPolicy: continue

Use 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:

bash
podman pod stats podman-pod-demo --no-stream

Sample output:

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  181

Each 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:

text
--exit-policy=continue

continue

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:

bash
podman pod create --exit-policy=stop --name exit-stop-pod

Start it and add a short-lived member:

bash
podman pod start exit-stop-pod

Add a container that exits after five seconds:

bash
podman run -d --pod exit-stop-pod --name exit-app registry.access.redhat.com/ubi9/ubi-minimal sleep 5

After five seconds, check pod status:

bash
podman pod ps --filter name=exit-stop-pod --format '{{.Name}} {{.Status}}'

Sample output:

output
exit-stop-pod Exited

Both 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:

bash
podman pod create --restart=on-failure --name restart-demo

The 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:

bash
podman pod rm exit-stop-pod

Removing a running pod without force fails:

bash
podman pod rm podman-pod-demo

Sample output (truncated):

output
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 improper

Force removal stops and deletes all members plus the infra container:

bash
podman pod rm -f podman-pod-demo

podman 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:

text
Pod:
  app ↔ log-shipper via 127.0.0.1:8080

User-defined network:
  frontend → api:8080 → database:5432

See 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 play is the path for YAML import, not podman pod create alone

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


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.


Frequently Asked Questions

1. What is a Podman pod?

A Podman pod is a group of containers that share selected Linux namespaces and can be managed together. By default members share network, IPC, and UTS namespaces but not PID. An infra container keeps those shared namespaces alive while application containers start and stop.

2. Do Podman pods share the PID namespace by default?

No. The default --share value is ipc,net,uts. PID is not shared unless you append it with --share=+pid. Using --share=pid alone replaces the default list with only PID, which removes network and IPC sharing.

3. Why does a Podman pod have an infra container?

The infra container is a lightweight holder process, usually catatonit, that keeps the pod shared namespaces alive independently of application containers. It starts before member containers and normally remains while the pod exists.

4. Do I need the k8s.gcr.io/pause image for Podman pods?

No on modern Podman. Podman creates a local infra container by default and does not require pulling the legacy Kubernetes pause image unless you override it with --infra-image.

5. When should I use a pod instead of containers on a user-defined network?

Use a pod when members need the same network namespace and communicate over localhost, or when joint lifecycle matters. Use separate containers on a user-defined network when each service needs its own IP, DNS names, and looser coupling.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)