Podman Architecture: How Daemonless Containers Actually Run

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
conmon-2.2.1-2.el10.x86_64
crun-1.27-2.el10_2.x86_64
Applies to Any Linux host with Podman 4.x or 5.x using netavark (default on current RHEL and Fedora)
Privilege Root for these examples (rootful Podman)
Scope Internal component layout — libpod, storage, conmon, OCI runtime, namespaces, cgroups, netavark, optional API service. Does not repeat introductory Podman concepts, install steps, resource-limit flags, or full networking mode reference.
Related guides What is Podman?
Podman vs Docker

You already know Podman runs containers without dockerd. This chapter shows what actually happens on the host: which libraries prepare storage and networking, which processes stay running after podman run returns, and how the kernel isolates the workload.

Every inspection command below uses one long-running container named podman-architecture-demo so you can correlate names with real PIDs, namespace IDs, and cgroup paths.


Podman architecture at a glance

Podman is not one binary doing everything. The CLI loads libpod, which coordinates image libraries, local storage, networking, and runtime configuration. libpod starts conmon and invokes an OCI runtime (crun on this host). The runtime configures Linux isolation; the workload then runs as an ordinary host process with extra namespaces.

text
User
podman CLI
libpod
 ├── containers/image
 ├── containers/storage
 ├── Buildah functionality
 ├── Netavark / Aardvark DNS
 └── configuration and state
conmon
OCI runtime (crun/runc)
Linux kernel
 ├── namespaces
 ├── cgroups
 ├── capabilities
 ├── seccomp
 └── SELinux/AppArmor
Container process

Rootful bridge networking on this host goes through netavark. Rootless setups often hand packets to the host through pasta instead of creating a bridge the unprivileged user cannot manage:

text
Rootless Podman
    pasta
Host networking

The rest of the article walks that stack top to bottom on a live container.


What happens when you run podman run?

Start a detached nginx container. I use the libpod test image because it stays healthy in the background while you inspect processes:

bash
podman run -d --name podman-architecture-demo quay.io/libpod/alpine_nginx:latest

Podman prints the full container ID when the create and start sequence succeeds:

output
5e938bc665bec01777e0b7e618d6009443b1afb8d1c9a11c917d3569cee66370

Confirm it is running before the host inspection commands:

bash
podman ps --filter name=podman-architecture-demo --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

Sample output:

output
NAMES                     STATUS                   PORTS
podman-architecture-demo  Up 15 seconds (healthy)  80/tcp

Here is the sequence behind that one command, in the order it happens on the host:

  1. The podman CLI parses flags, config files (containers.conf, storage.conf), and connection settings.
  2. containers/image resolves quay.io/libpod/alpine_nginx:latest — pull metadata if missing, verify signatures according to policy.
  3. containers/storage prepares a writable root filesystem from overlay layers and allocates runtime directories under runRoot.
  4. libpod builds an OCI runtime bundle (config.json), configures networking through netavark, and writes container state to the local database.
  5. libpod execs conmon, which in turn invokes crun with that bundle.
  6. crun creates namespaces, joins cgroups, applies capabilities, seccomp, and SELinux labels, then execs nginx.
  7. crun exits; conmon remains parent of the container process and forwards logs.
  8. The podman run CLI process exits. The container keeps running under conmon and nginx.

That flow is the spine of Podman architecture. The sections below zoom into each layer.


What is libpod?

libpod is the Go library at the center of Podman. The CLI is a thin front end; rootless helpers and optional API services also call into the same code.

libpod owns container lifecycle on disk and in memory:

  • Containers — create, start, stop, remove, inspect
  • Pods — groups that can share a network namespace
  • Volumes — named storage that survives container deletion
  • Runtime configuration — OCI bundle generation, hooks, security profiles
  • State — IDs, names, exit codes, health-check status
  • Integration — calls into storage drivers, netavark, and image copy logic

You do not browse libpod source to use Podman. Think of it as the coordinator: when you run podman run, libpod decides what to mount, which network to attach, which runtime binary to call, and where to record the result. When you run podman ps, libpod reads that state back.

Image pulls and local layer management go through containers/image and containers/storage — shared libraries also used by Buildah and Skopeo. podman build reuses Buildah code paths inside the same storage stack.


conmon and the OCI runtime

Two different programs handle “start the container” versus “watch it afterward.” Understanding which one survives on the host prevents false debugging trails.

conmon

conmon (container monitor) is a small C program libpod starts once per container. After startup it:

  • stays parent of the container’s main process (or tracks it closely, depending on configuration)
  • captures stdout and stderr according to the logging driver (journald on this host)
  • handles attach, tty allocation, and stdin forwarding when you use podman attach or run without -d
  • records the exit code and triggers cleanup hooks when the workload stops

conmon is not the OCI runtime. It outlives crun and remains even after your shell prompt returns from a detached podman run.

crun or runc

crun (or runc on other hosts) implements the OCI runtime specification. libpod hands it a bundle directory containing config.json. The runtime:

  • creates or joins namespaces listed in the spec
  • sets cgroup membership and controller limits from the spec
  • configures capabilities, seccomp, SELinux/AppArmor contexts
  • execs the container entrypoint (nginx here)

On success, the runtime process usually exits. Steady-state supervision belongs to conmon.

What you see on the host

Search the process list for monitor, runtime, and CLI processes tied to this container:

bash
ps -ef | grep -E 'conmon|crun|podman'

Sample output (trimmed; your SSH session may add extra lines):

output
root  67468  1  ... /usr/bin/conmon ... -n podman-architecture-demo -r /usr/bin/crun ...

The -r /usr/bin/crun argument shows which runtime conmon used. There is no long-lived crun line — it already finished. A podman line appears only while a CLI command is active.

View the parent/child relationship from conmon downward:

bash
pstree -ap "$(pgrep -f '[c]onmon.*podman-architecture-demo')"

Sample output:

output
conmon,67468 --api-version 1 -c 5e938bc665bec01777e0b7e618d6009443b1afb8d1c9a11c917d3569cee66370 ...
  `-nginx,67470
      |-nginx,67473
      |-nginx,67474
      `-nginx,67475

Read this tree carefully:

  • conmon (PID 67468) is a direct child of PID 1 — systemd adopted it after the short-lived podman run parent exited.
  • nginx (PID 67470) is the container’s main process on the host, not a child of your shell.
  • There is no permanent shell → podman → nginx chain after startup. Relationship changes once the CLI and runtime finish.

Inside the container’s PID namespace, that same nginx is PID 1. Map between views with:

bash
podman top podman-architecture-demo -eo pid,comm

Sample output:

output
PID COMMAND
    1 nginx
    9 nginx
   10 nginx
   11 nginx

Host PID 67470 corresponds to PID 1 inside the namespace.


Inspect the container's Linux namespaces

Namespaces are how the kernel gives each container its own view of mounts, process IDs, hostnames, and network interfaces. Count them from evidence, not from a textbook diagram.

Read the host PID libpod reports:

bash
podman inspect --format '{{.State.Pid}}' podman-architecture-demo

Sample output:

output
67470

List namespaces for that PID with lsns:

bash
lsns -p 67470

Sample output:

output
NS TYPE   NPROCS   PID USER COMMAND
4026531834 time      320     1 root /usr/lib/systemd/systemd ...
4026531837 user      317     1 root /usr/lib/systemd/systemd ...
4026532496 net         4 67470 root nginx: master process nginx -g daemon off;
4026532601 mnt         4 67470 root nginx: master process nginx -g daemon off;
4026532602 uts         4 67470 root nginx: master process nginx -g daemon off;
4026532603 ipc         4 67470 root nginx: master process nginx -g daemon off;
4026532604 pid         4 67470 root nginx: master process nginx -g daemon off;
4026532605 cgroup      4 67470 root nginx: master process nginx -g daemon off;

This container exposes eight namespace types in lsns. Two are shared with the host init process; six are private to the container workload:

Namespace Shared or private What it isolates on this host
time Shared with PID 1 Boot and monotonic clocks (same time namespace as the host)
user Shared with PID 1 UID/GID mappings — expected for rootful Podman without a separate user namespace
net Private Network devices, addresses, routing, and ports
mnt Private Mount table — container root filesystem and bind mounts
uts Private Hostname and NIS domain name
ipc Private System V IPC and POSIX message queues
pid Private Process IDs — only container processes visible as PID 1, 9, 10, 11 inside
cgroup Private cgroup hierarchy view for the container

Rootless Podman typically adds an unshared user namespace so UID 0 in the container maps to a high host UID. See Rootless Podman for mapping details; this rootful lab keeps the host user namespace.


How Podman uses cgroups v2

cgroups (control groups) account for and limit CPU, memory, PIDs, and I/O. Modern Podman on systemd hosts expects cgroup v2 unified hierarchy.

Check what the host reports:

bash
podman info --format 'cgroupManager={{.Host.CgroupManager}} cgroupsVersion={{.Host.CgroupsVersion}}'

Sample output:

output
cgroupManager=systemd cgroupsVersion=v2

cgroupManager=systemd means libpod asks systemd to create scoped units for containers on this host. That choice affects the path you see in /proc.

Read the cgroup membership file for the container’s main process (substitute the PID from podman inspect if yours differs):

bash
cat /proc/$(podman inspect --format '{{.State.Pid}}' podman-architecture-demo)/cgroup

Sample output:

output
0::/machine.slice/libpod-5e938bc665bec01777e0b7e618d6009443b1afb8d1c9a11c917d3569cee66370.scope/container

On this rootful host, the container sits under machine.slice in a libpod-<id>.scope unit, with the workload in a container cgroup below that. That path is what this machine shows right now — not a universal constant.

Rootless Podman places containers under the user’s delegated cgroup subtree (often user.slice/user@<uid>.service/...). Podman version, --cgroup-manager settings, and whether you run inside a user session all change the string. Always discover the path from /proc/<pid>/cgroup or podman inspect on the system you debug.

cgroups are the knob tree for resource limits. This article does not cover --memory or --cpus flags — see Podman resource limits for that workflow.


Podman image and storage architecture

Images and containers share a storage stack built on containers/storage with containers/image handling registry transport and signatures.

Ask Podman where data lives on this host:

bash
podman info --format 'graphRoot={{.Store.GraphRoot}} runRoot={{.Store.RunRoot}} volumePath={{.Store.VolumePath}} driver={{.Store.GraphDriverName}}'

Sample output:

output
graphRoot=/var/lib/containers/storage runRoot=/run/containers/storage volumePath=/var/lib/containers/storage/volumes driver=overlay
Path / driver Role
graphRoot Image layers and per-container writable overlay directories
runRoot Ephemeral runtime state — mount namespaces, pid files, transient locks
volumePath Named volumes created with podman volume create
overlay driver Copy-on-write layer stack merged into the container root filesystem

When podman run starts, storage assembles a merged root from image layers plus a thin writable layer. Removing the container deletes that writable layer unless you committed it to a new image.

Rootful storage defaults to /var/lib/containers/storage. Rootless users store under ~/.local/share/containers/storage (or paths set in $HOME/.config/containers/storage.conf). Administrators can relocate graphRoot and runRoot in /etc/containers/storage.conf. For cleanup, quotas, and migration, see Podman storage location.


Podman networking architecture

Current Podman releases default to netavark for network configuration and firewall integration. Aardvark-dns provides embedded DNS on user-defined networks. Older releases used the CNI plugin stack; netavark replaced that default on Fedora and RHEL 9+.

text
Podman
Netavark
  ├── network configuration
  ├── routing
  └── firewall integration

Aardvark DNS
  └── container name resolution

Confirm the active backend:

bash
podman info --format 'networkBackend={{.Host.NetworkBackend}}'

Sample output:

output
networkBackend=netavark

Inspect the default bridge network and the demo container’s address:

bash
podman network inspect podman

Sample output (trimmed):

output
[
     {
          "name": "podman",
          "driver": "bridge",
          "network_interface": "podman0",
          "subnets": [
               {
                    "subnet": "10.88.0.0/16",
                    "gateway": "10.88.0.1"
               }
          ],
          "containers": {
               "5e938bc665bec01777e0b7e618d6009443b1afb8d1c9a11c917d3569cee66370": {
                    "name": "podman-architecture-demo",
                    "interfaces": {
                         "eth0": {
                              "subnets": [
                                   {
                                        "ipnet": "10.88.0.20/16",
                                        "gateway": "10.88.0.1"
                                   }
                              ]
                         }
                    }
               }
          }
     }
]

Netavark created podman0, attached a veth pair, and assigned 10.88.0.20 to eth0 inside the container’s network namespace. The namespace path is visible from inspect:

bash
podman inspect --format '{{.NetworkSettings.SandboxKey}}' podman-architecture-demo

Sample output:

output
/run/netns/netns-fb4c7cb7-88da-389e-05bd-4d1b80b3da61

That file is a bind mount into an isolated network namespace. Rootless bridge mode is not always available; unprivileged workflows often use pasta to forward traffic without creating host bridges:

text
Podman
pasta
host networking

Port publishing, macvlan, and custom networks are out of scope here — see Podman networking.


Where Buildah and Skopeo fit

Podman is one binary in the Containers ecosystem. Three names often appear together:

Tool Role
Podman Run and manage containers, pods, networks, and volumes locally
Buildah Build images layer by layer (podman build calls Buildah libraries internally)
Skopeo Copy and inspect images between registries and local storage without running a container

All three share containers/image and containers/storage. Because Podman, Buildah, and Skopeo can use the same containers/storage backend, an image copied by Skopeo into the containers-storage: transport can appear directly in podman images, and a podman build layer lands in the same graph root as a pulled image.

You can install only podman and ignore the other commands, but recognizing the split explains log lines that mention Buildah versions during builds or Skopeo during registry mirroring jobs.


If Podman is daemonless, what is podman system service?

Local podman run does not require a permanently running manager like dockerd. Each CLI invocation loads libpod, does its work, and exits. Your container continues under conmon.

Podman can still expose a REST API compatible with Docker clients when you start the optional service:

text
Podman/Docker-compatible client
       Unix socket
podman system service
         libpod

Typical uses:

  • remote podman clients over SSH or TCP
  • Docker API consumers pointed at /run/podman/podman.sock
  • systemd socket activation — podman.socket starts the service on first connection

On this lab host the socket is not active until you enable it:

bash
systemctl is-active podman.socket

Sample output:

output
inactive

That inactive state is normal for CLI-only workflows. The service is an optional front door to the same libpod engine, not proof that everyday Podman secretly runs a central daemon. Detached containers do not depend on the API listener staying up.


Rootful vs rootless architecture

The same libpod code paths run rootful and rootless; privileges and kernel delegation change the details you saw above.

Area Rootful (this article) Rootless
Privileges Runs as root; full host capability set by default Runs as the login user; no host root required
User namespace Often shares host user namespace (as lsns showed) Dedicated user namespace; UID 0 maps to high host UID
Storage /var/lib/containers/storage ~/.local/share/containers/storage by default
Runtime directory /run/containers/storage Under /run/user/<uid>/
cgroup hierarchy systemd machine.slice / libpod-….scope on this host User delegated tree under user.slice
Networking netavark bridge (podman0) Often pasta or rootless netavark with slirp/pasta helpers
Visibility Root sees all rootful containers on the host User sees only containers in their own namespace/storage

Configuration steps for rootless mapping belong in Rootless Podman. This chapter only maps how architecture differs once you are already running.


Clean up the demo container

Remove the nginx container when you finish inspecting:

bash
podman rm -f podman-architecture-demo

Podman stops nginx, tears down the network namespace, and deletes the writable overlay layer. A follow-up podman ps -a --filter name=podman-architecture-demo should return no rows.


References


Summary

Podman architecture is a pipeline, not a single long-lived daemon. The CLI loads libpod, which coordinates image and storage libraries, prepares an OCI bundle, configures netavark networking, and starts conmon. conmon invokes crun to create namespaces and cgroups; crun execs nginx and exits while conmon keeps supervising.

On the lab host, lsns showed six container-private namespaces plus shared time and user namespaces — expected for rootful Podman. pstree proved the steady-state tree is conmon → nginx, not shell → podman → nginx. The cgroup path under machine.slice came from this rootful systemd host; rootless machines show different strings under user.slice.

Storage paths came from podman info: overlay layers under graphRoot, ephemeral state under runRoot. netavark attached the demo container to podman0 at 10.88.0.20. The optional podman system service exposes the same libpod engine to remote clients without changing how a normal detached podman run behaves.

When something breaks, match the symptom to the layer — pull errors in image/storage, start failures in crun/conmon logs, DNS in aardvark-dns, permissions in user namespaces. Next, practice lifecycle commands in Run containers with podman run or tune networks in Podman networking.


Frequently Asked Questions

1. Does Podman use containerd?

No. Ordinary local Podman does not use containerd. Podman uses libpod, containers/storage, containers/image, conmon, and an OCI runtime such as crun or runc, so podman run does not require a running containerd daemon.

2. What is conmon in Podman?

conmon is a small monitor process Podman starts for each container. It tracks the main container process, forwards logs, handles attach and stdin, records exit codes, and notifies Podman when the container stops. One conmon process remains per running container after the CLI exits.

3. Why is crun missing from ps after the container starts?

crun is invoked to create namespaces, apply the OCI bundle, and exec the container entrypoint. It typically exits once the container process is running. conmon stays behind as the per-container supervisor; that is the steady-state process tree you inspect on the host.

4. Where does Podman store images and containers?

Paths come from podman info and containers storage.conf. On this rootful lab host, graphRoot is /var/lib/containers/storage and runRoot is /run/containers/storage. Rootless users normally store persistent container storage under their home directory, while runtime state lives under their per-user runtime directory. Administrators can override defaults in storage.conf.

5. Does podman system service mean Podman is not daemonless?

Normal local podman commands load libpod in-process and exit while containers keep running. podman system service is an optional REST API listener for remote or Docker-compatible clients. Socket activation can start it on demand; it does not replace the per-command daemonless model for everyday CLI use.
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)