Chapter 68: Podman in Depth: Install, Rootless Containers, Pods, Compose, Kube YAML and Alternatives
68.3 Podman pods: containers that share a network, exactly like Kubernetes pods
Why: the problem
Docker has containers and networks. Kubernetes has pods. Podman is the only common local tool that has both. A Podman pod is a group of containers that share one network namespace, one IP address, one set of published ports and optionally the IPC and UTS namespaces. Inside a pod, the orders API reaches Postgres on localhost:5432 and Redis on localhost:6379, exactly as it would inside a Kubernetes pod. There is no service name, no DNS lookup and no bridge between them, because they are, from the network point of view, the same machine. How does that work? When you create a pod, Podman starts a tiny infra container, built from the catatonit or pause image, whose only job is to hold the namespaces open. Every container you add to the pod joins those namespaces. If the API crashes and restarts, the IP and ports stay the same because the infra container keeps them. This is the same trick the kubelet uses with the pause container on every Kubernetes node, which is why skills you learn here transfer directly. Why use pods locally? Three reasons. First, realistic testing: a developer at Swiggy who builds a service with a log-shipping sidecar can run the exact sidecar layout on a laptop before writing a single line of Kubernetes YAML. Second, simple networking: ports are published once on the pod, not on each container, so there is one place to look when something is not reachable. Third, a direct path to Kubernetes: podman generate kube turns a running pod into a Pod or Deployment manifest, which we use in 68.5. In this lesson we create a pod for the orders app, add Postgres, Redis and the API, publish one port, prove the containers talk over localhost, add a sidecar that tails logs through a shared volume, and learn the pod lifecycle commands: start, stop, restart, inspect, stats and rm. We also see the limits: two containers in one pod cannot both listen on the same port, and a pod runs on one host only, so it is not a replacement for Kubernetes scheduling.
What: every term in plain English
- Pod — Group of containers sharing a network namespace and ports.
- Infra container — Tiny container that holds the pod's namespaces open.
- catatonit — Minimal init process used as the default infra command.
- --pod — Flag on podman run that places a container inside a pod.
- -p on the pod — Ports are published on the pod, not on member containers.
- --share — Chooses which namespaces are shared: net, ipc, uts, pid.
- Sidecar — Helper container that runs beside the main app in the same pod.
- Init container — Container that runs to completion before the others start, --init-ctr.
- podman pod stats — Live CPU and memory for every container in pods.
- Pod restart policy — How containers restart, set with --restart on members.
How: step by step
- Create an empty pod with published ports. Pod and infra container exist.
- Run Postgres inside the pod. Listens on localhost:5432 of the pod.
- Run Redis inside the pod. Listens on localhost:6379.
- Run the API inside the pod. Connects to both over localhost.
- Call the API from the host. Answer on port 8080.
- Add a log sidecar with a shared volume. Sidecar prints app logs.
- Use pod lifecycle commands. Stop, start, restart, stats.
- Remove the pod and all members. Clean host.
A simple way to picture a pod is a shared flat with one front door. The flat has one address, which is the pod IP, and one doorbell panel, which is the published ports. Every flatmate, meaning each container, has their own room and their own belongings, but they all share the kitchen and the corridor, which is localhost. When one flatmate moves out and a new one moves in, the address does not change, because the building manager, the infra container, still holds the lease. The only rule is that two flatmates cannot use the same doorbell number. Keep this picture in mind when you design a pod: put only containers in it that truly need to share the flat, and keep everything else in its own pod on a shared network.
Example 1: Create the orders pod with Postgres, Redis and the API
Goal. Create the orders pod with Postgres, Redis and the API.
pod.sh
set -euo pipefail
printf '%s' 'REPLACE_WITH_DB_PASSWORD' | podman secret create pg_pass -
podman pod create --name orders -p 8080:8080 --label app=orders
podman run -d --pod orders --name db \
--secret pg_pass,type=env,target=POSTGRES_PASSWORD \
-e POSTGRES_USER=orders -e POSTGRES_DB=orders \
-v orders-pg:/var/lib/postgresql/data docker.io/library/postgres:16-alpine
podman run -d --pod orders --name cache docker.io/library/redis:7-alpine
podman run -d --pod orders --name api \
--secret pg_pass,type=env,target=DB_PASSWORD \
-e DB_HOST=localhost -e DB_USER=orders -e REDIS_HOST=localhost \
registry.example.com/shop/orders-api:1.4.0
podman pod ps
podman ps --pod --format 'table {{.Names}}\t{{.PodName}}\t{{.Status}}'
curl -s localhost:8080/health
What each line does
podman secret create— Stores the placeholder password once; containers read it as an env var, never on the command line.pod create -p 8080:8080— The only published port; Postgres and Redis stay private inside the pod.--pod orders— Each container joins the pod's network namespace.DB_HOST=localhost— Inside a pod, other members are on localhost, exactly like Kubernetes.ps --pod— Shows which pod each container belongs to, including the infra container.
Green output
$ bash pod.sh
5f1c2a...
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
9b8e7d6c5a4f orders Running 5 seconds ago 1a2b3c4d5e6f 4
NAMES PODNAME STATUS
9b8e7d6c-infra orders Up 5 seconds
db orders Up 4 seconds
cache orders Up 3 seconds
api orders Up 2 seconds
{"status":"ok","db":"up","redis":"up"}
Expected result. Four containers, one of them the infra container, one IP and one published port. The API found Postgres and Redis on localhost without any network or DNS setup, which is exactly how the same containers will talk inside a Kubernetes pod.
Example 2: Add a log-shipping sidecar with a shared volume
Goal. Add a log-shipping sidecar with a shared volume.
sidecar.sh
podman volume create orders-logs
podman rm -f api
podman run -d --pod orders --name api \
--secret pg_pass,type=env,target=DB_PASSWORD \
-e DB_HOST=localhost -e DB_USER=orders -e REDIS_HOST=localhost \
-e LOG_FILE=/logs/app.log -v orders-logs:/logs \
registry.example.com/shop/orders-api:1.4.0
podman run -d --pod orders --name shipper -v orders-logs:/logs:ro \
docker.io/library/busybox:1.36 tail -F /logs/app.log
for i in 1 2 3; do curl -s -o /dev/null localhost:8080/orders; done
podman logs --tail 3 shipper
podman pod inspect orders --format '{{.Name}} {{.NumContainers}} {{.SharedNamespaces}}'
What each line does
orders-logs volume— Shared folder between the app and the sidecar, like an emptyDir in Kubernetes.:ro— The sidecar can only read logs, it cannot change them.tail -F— Stands in for a real shipper such as Fluent Bit or Vector.SharedNamespaces— Shows net, ipc and uts are shared by default; pid is not unless you ask.
Green output
$ bash sidecar.sh
GET /orders 200 12ms
GET /orders 200 9ms
GET /orders 200 8ms
orders 5 [ipc net uts]
Expected result. The sidecar saw every request without the API knowing it exists. This is the same pattern Swiggy, Zomato and Flipkart use in Kubernetes to ship logs, and you tested it on one laptop with two podman run commands.
Example 3: Pod lifecycle, stats, init containers and the port clash
Goal. Pod lifecycle, stats, init containers and the port clash.
lifecycle.sh
podman pod stop orders && podman pod ps --format '{{.Name}} {{.Status}}'
podman pod start orders && podman pod ps --format '{{.Name}} {{.Status}}'
podman pod stats --no-stream --format 'table {{.Name}}\t{{.CPU}}\t{{.MemUsage}}'
podman create --pod orders --init-ctr once --name migrate \
registry.example.com/shop/orders-api:1.4.0 ./migrate up
podman run -d --pod orders --name web2 docker.io/library/nginx:1.27-alpine
podman run -d --pod orders --name web3 docker.io/library/nginx:1.27-alpine || true
podman logs web3 | tail -1
podman pod rm -f orders && podman volume rm orders-pg orders-logs && podman secret rm pg_pass
What each line does
pod stop and start— Act on every member, in dependency order, keeping the same pod IP.pod stats— One table for all members, handy for finding which container uses memory.--init-ctr once— Runs the migration the first time the pod starts, like a Kubernetes init container.web3 fails— Two nginx containers both want port 80 in the same network namespace; only one can bind it.pod rm -f— Removes the pod and every member in one command.
Green output
$ bash lifecycle.sh
orders Exited
orders Running
NAME CPU % MEM USAGE / LIMIT
api 0.41% 38.2MB / 8.1GB
db 0.12% 31.7MB / 8.1GB
cache 0.08% 4.9MB / 8.1GB
shipper 0.00% 0.6MB / 8.1GB
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)
9b8e7d6c5a4f...
Expected result. Pod commands act on the whole group, init containers run once, and the port clash shows the one real rule of pods: members share ports, so each must listen on its own. Kubernetes enforces exactly the same rule.
Every command, step by step (Podman lab)
-
Create
podman pod create --name orders -p 8080:8080What it does: New pod.
9b8e7d6c5a4f -
List
podman pod psWhat it does: Pods.
orders Running 1 -
Join
podman run -d --pod orders --name db postgres:16-alpineWhat it does: Member.
5f1c2a... -
Members
podman ps --podWhat it does: Pod column.
db orders -
Health
curl -s localhost:8080/healthWhat it does: API.
{"status":"ok"} -
Inspect
podman pod inspect ordersWhat it does: Details.
SharedNamespaces: ipc net uts -
Stats
podman pod stats --no-streamWhat it does: Usage.
api 0.41% -
Stop
podman pod stop ordersWhat it does: Stop all.
orders -
Start
podman pod start ordersWhat it does: Start all.
orders -
Remove
podman pod rm -f ordersWhat it does: Clean up.
9b8e7d6c5a4f
When it is red 1
Error: invalid config provided: published or exposed ports must be defined when the pod is created
Cause. Tried -p on a container that joins a pod.
Fix, step by step
- Publish ports with podman pod create -p, or recreate the pod with the port.
When it is red 2
Address in use
Cause. Two members listen on the same port.
Fix, step by step
- Change one container's listen port; pods share ports like Kubernetes pods.
When it is red 3
connection refused to db:5432
Cause. Used a container name instead of localhost inside the pod.
Fix, step by step
- Use localhost for pod members, or put containers on a network instead of a pod.
When it is red 4
Error: pod orders already exists
Cause. Old pod left behind.
Fix, step by step
- podman pod rm -f orders, then create again.
When it is red 5
image not known: localhost/podman-pause
Cause. Infra image could not be built or pulled.
Fix, step by step
- Run podman system migrate, or set --infra-image registry.k8s.io/pause:3.10.
Practice tasks
Easy Task 1: Create
Why: Pod.
What to do: Create a pod with one port.
- pod ps.
Solution
podman pod psEasy Task 2: Join
Why: Member.
What to do: Add Redis to the pod.
- ps --pod.
Solution
podman ps --podEasy Task 3: Localhost
Why: Talk.
What to do: Ping Redis from another member on localhost.
- PONG.
Solution
podman exec api redis-cli -h localhost pingMedium Task 4: Postgres
Why: Secret.
What to do: Run Postgres with a Podman secret.
- Ready.
Solution
podman logs dbMedium Task 5: Sidecar
Why: Logs.
What to do: Add a log sidecar via a shared volume.
- Lines shown.
Solution
podman logs shipperMedium Task 6: Stats
Why: Usage.
What to do: Read pod stats.
- Table.
Solution
podman pod stats --no-streamHard Task 7: Clash
Why: Ports.
What to do: Reproduce and fix a port clash.
- One listens on 8081.
Solution
podman logs web3Hard Task 8: Tracker
Why: Main task.
What to do: Full tracker pod.
- All up.
Solution
podman pod inspect trackerHomework: try at home
For a Swiggy-style delivery tracker, create a pod named tracker publishing 9090, run Postgres, Redis and the tracker API inside it with a Podman secret for the placeholder password, add a busybox sidecar tailing a shared log volume, add a once init container that prints 'migrated', then show podman pod stats and remove everything.
Recap
- A Podman pod is a group of containers sharing one network namespace and ports.
- An infra container holds the namespaces, like the pause container in Kubernetes.
- Members reach each other on localhost; ports are published on the pod.
- Sidecars and init containers work the same way as in Kubernetes.
- Pods run on one host; for scheduling across hosts you still need Kubernetes.
FAQ
Q. Pod or network, which should I use?
Use a pod when containers are tightly coupled and must share localhost, like an app and its sidecar. Use a Podman network when services are independent and should find each other by name, like several microservices. Compose files with Podman create a network by default, and can use a pod if you ask for it.
Q. Can a container be in two pods?
No. A container joins exactly one pod at creation time. To move it, remove it and create it again with a different --pod value.
Q. Can I share the PID namespace?
Yes, podman pod create --share net,ipc,uts,pid. Then ps inside one member shows processes of all members, useful for debugging sidecars.
Q. Does Docker have pods?
No. Docker Compose groups containers per project but each has its own network namespace. The closest trick is network_mode: service:api, which shares one container's network with another.
Q. What happens to the pod IP on restart?
The pod IP and published ports belong to the infra container, so restarting the API or Postgres keeps them. Only removing and recreating the pod gives a new IP, exactly like deleting a Kubernetes pod.
Q. Can I limit resources for a whole pod?
Yes, podman pod create accepts --cpus and --memory, which set a cgroup for the pod; members also keep their own limits, like Kubernetes pod-level resources.
Interview questions
-
What is the infra container in a Podman pod?
Model answer: A tiny container that creates and holds the shared namespaces so members can restart without losing the IP and ports.
-
How are Podman pods related to Kubernetes pods?
Model answer: Same model: shared network, localhost communication, sidecars and init containers; generate kube exports them as Pod manifests.
-
Why can't two pod members use the same port?
Model answer: They share one network namespace, so only one socket can bind a given port.
-
When would you choose a pod over a network?
Model answer: For tightly coupled containers such as an app plus log or proxy sidecar.
Quick quiz
Q1. What does a pod share by default?
Network, IPC and UTS namespaces.
Q2. Where are ports published?
On the pod, at pod create time.
Q3. How do pod members reach each other?
On localhost.
Q4. What holds the namespaces?
The infra container.
Q5. Which flag adds an init container?
--init-ctr.
Samajla ka?