deep·tech·intuition
intermediate ·

Podman Deep Intuition

An experienced engineer's guide to Podman

1. One-Sentence Essence

Podman is a Linux container engine that runs each container as an ordinary child process of your shell, owned by your user account, with no privileged background daemon mediating anything.

Everything interesting about Podman — its security story, its systemd story, its quirks on macOS, its rough edges around Compose — falls out of that one sentence. If you internalize nothing else from this document, internalize that. Containers in Podman are not “managed by Podman” in the sense that Docker containers are managed by dockerd. They are managed by Linux — by systemd, by your user session, by the kernel — and Podman is just the CLI that knows how to ask the kernel to set them up.

This is not a marketing reframe. It’s a structural difference that changes how you reason about every other behavior the tool exhibits.


2. The Problem It Solved

Before Podman, the dominant way to run containers on Linux was Docker, and Docker’s architecture had a problem that grew more uncomfortable every year: it required a long-running root daemon called dockerd.

That daemon was the only thing that could touch container internals. Every docker command you typed was really an RPC to the daemon over a Unix socket. The daemon held the container state, the image cache, the network configuration, everything. And it ran as root, because creating containers requires kernel operations (mounting filesystems, creating network namespaces, setting up cgroups) that only root can do.

For a long time, nobody really cared. Then it started biting people in three different ways at once.

The security problem. If your user could talk to the Docker socket, your user could become root — trivially. docker run -v /:/host -it alpine chroot /host and you own the machine. The Docker socket was effectively a root grant in a trench coat. Audit teams hated this. Compliance frameworks hated this. Multi-tenant CI hated this. “Docker-in-Docker” was the standard hack for letting CI jobs build images, and it was a known-bad pattern with no clean fix while the daemon model persisted.

The reliability problem. When dockerd crashed or hung, everything using containers on that host stopped. Logs stopped flowing. Containers couldn’t be restarted. systemd, the actual init system on every modern Linux box, had no idea your containers existed — they were children of dockerd, not of systemd. Restarts on boot meant trusting dockerd to come back up cleanly and then trusting it to start the right things in the right order. It was a parallel universe of process management that sat alongside the one Linux already had.

The Linux-philosophy problem. Linux has a perfectly good way to manage long-running processes: systemd. It does dependency ordering, restart policies, logging, cgroup management, socket activation. Docker reinvented all of that, badly, in a single binary. To experienced sysadmins this looked like a NIH-style bypass of a perfectly good init system.

Red Hat — whose entire business depends on running Linux servers — was particularly unhappy about all three problems. So a team built libpod, eventually fronted by a CLI called podman, that does exactly what Docker does without the daemon. Each podman command directly invokes the same low-level Linux primitives Docker invokes through its daemon. When the container starts, it becomes a child of your shell (or systemd), supervised by a tiny helper called conmon. Podman itself exits. There is no podmand. There never will be.

That decision — no daemon — is the design choice that produces almost everything else that distinguishes Podman from Docker.


3. The Concepts You Need

Containers as a topic carry a lot of vocabulary, and Podman adds a small layer on top. You need to be fluent in the lower-layer concepts before Podman’s behaviors will make sense. This section is the prerequisite glossary.

Linux primitives that make containers possible

  • Namespace. A kernel feature that gives a process a private view of some part of the system. There are several kinds (PID, mount, network, UTS, IPC, user, cgroup). A container is, fundamentally, a process running inside a bundle of fresh namespaces. “Containers” aren’t a thing the kernel knows about. Namespaces are.
  • User namespace. The most important namespace for understanding Podman specifically. Inside a user namespace, UIDs are mapped to different UIDs outside. UID 0 inside (root) can map to UID 100000 outside (an unprivileged account). This is what makes “rootless” containers possible — the process thinks it’s root, but the kernel sees it as someone with no privileges.
  • cgroup (control group). The kernel mechanism for resource limits and accounting: CPU, memory, I/O, PIDs. Modern Linux uses cgroups v2, a unified hierarchy that rootless Podman depends on for clean resource accounting.
  • subuid / subgid. Two files (/etc/subuid, /etc/subgid) that allocate ranges of UIDs/GIDs each user is allowed to map into their user namespace. Without an entry here, rootless containers cannot work. This is where 99% of “why doesn’t this work on my machine” troubleshooting ends up.
  • OCI (Open Container Initiative). The industry standard for what a container image is and what a container runtime does. An image is a tarball of layers plus a JSON manifest. A runtime is a binary that knows how to take an OCI bundle (filesystem + config) and turn it into a running process. Podman, Docker, and Kubernetes all consume OCI images. They are interoperable at the image level.

Podman’s process model

  • OCI runtime. A small binary that takes a bundle and tells the kernel to start the container. Podman supports runc (the original, Go) and crun (a faster C implementation, ~300KB vs runc’s ~15MB). On modern systems with cgroups v2, Podman defaults to crun. The OCI runtime’s job is to instrument the kernel to control how PID 1 of the container runs. After it finishes setting up the kernel and executing PID 1, the OCI runtime exits. This is critical: the runtime exits. It doesn’t stick around.
  • conmon. Short for “container monitor.” A tiny C program that wraps each container. Conmon is a monitoring program and communication tool between a container manager (like Podman or CRI-O) and an OCI runtime (like runc or crun) for a single container. One conmon per container. It holds the container’s stdio open, writes logs, and records the exit code when the container dies. Conmon is what stays alive after podman run returns. It is not a daemon in the Docker sense — it’s a per-container babysitter.
  • libpod. The Go library that implements all of Podman’s logic. The podman CLI is essentially a thin wrapper over libpod. Anything Podman can do, libpod can do, which is why there are also bindings (Python, REST API) for it.

The container family of tools

Podman is part of a deliberately decomposed toolset. Each tool does one job. This is the inverse of Docker’s “one binary does everything” philosophy.

  • Podman. Runs and manages containers and pods. Pulls and pushes images.
  • Buildah. Builds OCI images. You can use it directly, or just call podman build — under the hood, that’s Buildah being invoked.
  • Skopeo. Moves images around between registries, local storage, and tarballs. Inspects images without pulling them. The “image plumbing” tool.
  • CRI-O. A sibling project: a Kubernetes-specific container runtime that talks the Kubernetes CRI protocol. Same authors, same primitives, different audience. You won’t use CRI-O directly — Kubernetes uses it. But knowing it exists explains why Podman feels so Kubernetes-shaped: it shares DNA.

Podman-specific concepts

  • Pod. A group of containers that share namespaces (network, IPC, sometimes PID). Borrowed directly from Kubernetes. Every Podman pod has an invisible infra container that owns the shared namespaces and keeps them alive even when other containers in the pod restart. This is exactly how Kubernetes pods work.
  • Quadlet. A declarative way to run containers as systemd services. You write a .container file (looks like a systemd unit), drop it in ~/.config/containers/systemd/, and systemd generates a real .service file at boot time. This is the production deployment story for Podman on a single host.
  • Podman machine. A Linux VM that Podman manages for you on macOS and Windows, because containers are Linux. On Mac, this is usually a Fedora CoreOS VM running on Apple’s virtualization framework. You barely notice it exists — podman commands transparently forward into the VM.
  • Rootless vs rootful. Whether Podman is being run as a normal user (rootless, the default and the whole point) or via sudo (rootful, when you genuinely need privileges the kernel won’t grant otherwise). They have separate storage. A rootless container and a rootful container on the same machine can’t see each other’s images or volumes. This trips people up constantly.

If those terms don’t yet feel solid, the rest of this document will keep reinforcing them. They are the vocabulary you need; you’ll be using it from here on.


4. The Distilled Introduction

This section is the part that replaces ten hours of YouTube. It walks through Podman as a practitioner uses it: install it, run things, build things, network them, persist data, hook them into systemd. Every command is paired with what it actually does, not just what to type.

Installing it

On any modern Linux distribution, Podman is in your package manager:

# Fedora / RHEL / CentOS — already installed, but to be explicit:
sudo dnf install -y podman

# Debian / Ubuntu:
sudo apt update && sudo apt install -y podman

# Arch:
sudo pacman -S podman

On macOS or Windows you also need podman machine, which manages the underlying Linux VM. The easiest path is brew install podman on Mac, or install Podman Desktop for a GUI plus the machine plumbing. Then:

podman machine init    # download and create a VM
podman machine start   # boot it

From here on, every podman command on your Mac or Windows box is silently routed into that VM. You’ll forget it exists. That’s fine until you need to bind-mount a file from your host into a container, which we’ll cover in the gotchas section.

Once installed, verify with podman info. Look at the output. You’ll see runtime: crun, cgroupManager: systemd, networkBackend: netavark, and a security.rootless: true line. Memorize where this output lives — it’s the first thing you check when something doesn’t work.

Your first container

podman run --rm -it docker.io/library/alpine sh

Three things happened. Podman pulled the alpine image from Docker Hub (note: you specify the registry explicitly; Podman doesn’t assume Docker Hub by default unless you configure it). It then asked the OCI runtime to create a new set of namespaces and start sh inside them. The -it gave you an interactive terminal. --rm says “delete the container when the process exits.”

Now do something illuminating. Inside the container:

id          # uid=0(root) gid=0(root)
ps -ef      # only the container's processes are visible

You appear to be root. You appear to be in an empty system. Now in another terminal on the host:

ps -ef | grep sh           # find your shell process
id <that-pid's-owner>      # NOT root — it's you

That’s user namespaces working. The container thinks it’s root; the kernel knows it’s you. This is the whole security argument in five seconds of typing.

The lifecycle commands you’ll type 50 times a day

podman run -d --name web -p 8080:80 docker.io/library/nginx
# -d = detached (background)
# --name = give the container a name (otherwise it's a random adjective_noun)
# -p 8080:80 = map host port 8080 to container port 80

podman ps                  # list running containers
podman ps -a               # include stopped ones
podman logs -f web         # tail the container's stdout/stderr
podman exec -it web sh     # open a shell inside the running container
podman stop web            # graceful stop (SIGTERM, then SIGKILL after 10s)
podman rm web              # delete the stopped container
podman rm -f web           # force: stop + delete in one shot

If you know Docker, this is 100% the same. The CLI compatibility is not approximate — it’s deliberate, and the Podman team treats divergences as bugs. The mental shift isn’t in the commands; it’s in what’s happening underneath them.

Images

podman pull docker.io/library/postgres:16    # download an image
podman images                                  # list local images
podman rmi postgres:16                         # remove
podman image prune                             # clean up dangling images
podman system prune                            # nuclear: clean everything unused

Image references in Podman are fully qualified by default. nginx is not the same as docker.io/library/nginx. Podman wants to know which registry. You can configure shortcuts in /etc/containers/registries.conf (system-wide) or ~/.config/containers/registries.conf (user). The Fedora defaults will search registry.fedoraproject.org, registry.access.redhat.com, quay.io, and docker.io in that order. Most Linux distros set this up; if you’ve installed Podman by hand or on Mac, you may need to add docker.io to the search list or you’ll get prompts asking you to pick a registry.

Building images

podman build -t myapp:dev -f Dockerfile .

That Dockerfile is the same Dockerfile you’ve always used. Podman build is Buildah under the hood, but you don’t need to know that — podman build just works. Multi-stage builds, build args, BuildKit-style cache mounts (with --build-arg, --cache-from, etc.) are all supported.

The one shift: by default, Podman build is rootless too. If your Dockerfile does something that requires actual root (like installing certain system packages that touch device files), you may need podman build --userns=keep-id or build inside a rootful Podman. 95% of normal Dockerfiles build fine rootless.

Volumes and persistent data

Two kinds of mounts.

Named volumes, stored in ~/.local/share/containers/storage/volumes/ for rootless:

podman volume create pgdata
podman run -d --name pg \
    -e POSTGRES_PASSWORD=secret \
    -v pgdata:/var/lib/postgresql/data \
    docker.io/library/postgres:16

Bind mounts, mapping a host directory into the container:

podman run --rm -v $(pwd)/src:/app:Z -w /app alpine ls /app

That :Z (capital) is critical on SELinux systems (Fedora, RHEL, CentOS). It tells Podman to relabel the host directory so the container can actually access it. Lowercase :z does the same but allows sharing across containers. Omit this on Ubuntu/Debian (no SELinux). Omit this on Mac (SELinux happens inside the Linux VM, where the bind-mounted file already arrived through 9p/virtiofs and is fine).

This Z/z thing is the single most common “why can’t my container read this file” problem in the entire ecosystem.

Networking — and why rootless networking is its own world

For rootless containers, the default network is created by pasta (or slirp4netns on older systems) — a userspace network stack that translates between the container and the host without requiring root. The kernel won’t let you, an unprivileged user, create bridge devices or modify iptables. So Podman simulates a network in userspace instead.

What this means in practice:

podman run -p 8080:80 nginx
# This works. Maps host 8080 to container 80.

podman run -p 80:80 nginx
# This FAILS, rootless. You cannot bind privileged ports (<1024).

The fix is either to use a higher port and front it with a reverse proxy, or to lower the kernel’s privileged-port floor: sudo sysctl net.ipv4.ip_unprivileged_port_start=80. There are other fixes (setcap on the rootlessport helper, socket activation via systemd) but they’re more brittle.

Container-to-container networking is a place where the default network in rootless mode bites you. On Docker, when you run two containers, they can ping each other by container name on the default bridge. On rootless Podman with the default network, they can’t — DNS-based service discovery is only set up on custom networks. The fix is to create your own:

podman network create mynet
podman run -d --name db --network mynet postgres:16
podman run -d --name app --network mynet myapp
# Inside app, `db` now resolves to the container's IP.

If you only remember one operational detail from this section: always create a named network and put your containers in it. Don’t rely on the default.

Pods

This is where Podman starts diverging from Docker in capability, not just architecture.

podman pod create --name webstack -p 8080:80
podman run -d --pod webstack --name web nginx
podman run -d --pod webstack --name cache redis

Both containers share network namespace. Inside web, Redis is reachable at localhost:6379. Inside cache, nginx is at localhost:80. This is exactly how a Kubernetes pod works. You can stop the whole pod with podman pod stop webstack, restart it, generate Kubernetes YAML from it (podman kube generate webstack > web.yaml), and run that YAML elsewhere (podman kube play web.yaml) — including on an actual Kubernetes cluster.

Pods are the unit of grouping for containers that genuinely belong together: an app and its sidecar, a server and its log shipper. They are not a replacement for Compose for unrelated services that just need a shared network — for that, just use a named network.

Running things forever: systemd and Quadlet

For local one-shot experiments, podman run --restart always is fine. For anything that actually needs to come back after a reboot, the answer is systemd, and the modern way to wire Podman into systemd is Quadlet.

You write a file like this — call it ~/.config/containers/systemd/web.container:

[Unit]
Description=My nginx container
After=network-online.target

[Container]
Image=docker.io/library/nginx:latest
PublishPort=8080:80
Volume=/srv/www:/usr/share/nginx/html:ro,Z

[Service]
Restart=always

[Install]
WantedBy=default.target

Then:

systemctl --user daemon-reload
systemctl --user start web.service

systemd reads the .container file at boot, generates a real .service unit on the fly, and runs Podman accordingly. You get full systemd lifecycle management — proper logging via journalctl, restart policies, dependency ordering, the lot. You did not write any podman run command. The container’s lifecycle is now a normal Linux service, not a Podman-specific thing.

This is the production deployment story for a single host. Multi-host? You go to Kubernetes.

Compose, for the Docker refugees

podman compose up -d

In Podman 5+, the podman compose subcommand runs the official Docker Compose binary against Podman’s API socket. Most things work. Some things don’t (we’ll list them in Section 11). If you’ve got a docker-compose.yml you want to run on Podman, this is the path. For anything beyond local dev, generate Kubernetes YAML from your pods (podman kube generate) and let go of Compose entirely — it’s the right long-term direction.

What you now know

You can run, build, network, persist, and orchestrate containers with Podman. You can deploy them as systemd services that survive reboots. You can run multi-container apps as pods, in Compose, or as Kubernetes YAML. The rest of this document explains why the things that worked, worked, and why the things that didn’t, didn’t.


5. The Mental Model

There are three core ideas. Internalize these and everything else stops being arbitrary.

Core Idea 1: A Podman container is a process tree owned by your user, not by Podman.

This is the most consequential idea. When you run podman run, the podman binary exits within seconds. What’s left running is conmon (a tiny supervisor) and the container process, both children of either your shell or systemd. There is no central Podman service that has a list of “your containers.” Podman’s state is on disk, in ~/.local/share/containers/. When you next run podman ps, it reads that state from disk, asks the kernel which of the recorded processes are still alive, and shows you the answer.

This predicts:

  • If podman crashes mid-command, your existing containers keep running. There’s no daemon to take them down with it.
  • You can have one user run podman and another user run podman, and they will not see each other’s containers, even on the same machine. Container state is per-user.
  • systemd manages your containers natively. You don’t need a “service that watches the service” — there is no extra layer.
  • Reading container state requires disk access. If ~/.local/share/containers/ is on a slow disk or NFS, podman ps can be slow. Docker’s daemon caches that in RAM; Podman doesn’t.
  • A container that’s been “running” since last week is literally a process that’s been running since last week. You can find it in ps aux | grep conmon and read its /proc entry. There’s no abstraction layer.

Core Idea 2: “Root inside the container” is a lie the kernel tells the container, not a fact about the host.

User namespaces are the magic. Inside the container, the process sees UID 0. Outside, the kernel knows that UID 0 in this namespace is actually UID 100000 (or wherever your /etc/subuid allocation starts) on the host. The “root” inside the container has root’s privileges within its own user namespace — it can install packages, modify /etc, do whatever root inside a Linux box does — but it has no privileges in the host’s namespace.

This predicts:

  • A container escape doesn’t grant root on the host. It grants whatever the host UID is — usually nothing useful.
  • Bind-mounted files have surprising ownership inside the container. If you mount /home/you/data into the container, and a file in there is owned by you (host UID 1000), inside the container it appears owned by some random UID — because the container’s UID space is shifted. This is the source of essentially every rootless permission error.
  • --userns=keep-id exists to flatten this back: it maps your host UID to the same UID inside the container, so a file you own outside is owned by “you” inside. Use this for development bind mounts. Don’t use it for production — it defeats some of the isolation.
  • You cannot do things that require actual host root from inside a rootless container, even if you’re “root” inside. You cannot load a kernel module. You cannot bind to host port 80. You cannot mount block devices. The kernel checks privileges against the initial namespace for those operations.
  • Rootless networking has to be done in userspace (pasta/slirp4netns), because creating real bridge devices and modifying iptables is one of those host-root-required operations.

Core Idea 3: Podman is composed of small Linux-native parts, not a monolithic engine.

Image storage, image building, image pushing, container running, container monitoring, networking — these are all separate components. containers/storage handles disk. Buildah handles builds. containers/image library handles registry interaction (Skopeo wraps it). Conmon handles per-container supervision. Netavark handles networking. Each is its own project under the containers/ GitHub org. Podman is the CLI that orchestrates them.

This predicts:

  • You can use these pieces independently. Need to copy an image between registries without pulling it locally? Use Skopeo directly. Need to build an image inside a CI container without a Docker socket? Use Buildah directly. Need a tiny container runtime for embedded use? Use crun directly.
  • The blast radius of bugs is smaller. A networking bug in Netavark doesn’t touch image storage. Compare to Docker, where everything lives in one daemon and one bug can wedge the whole thing.
  • The “Podman ecosystem” is bigger than just podman. When you see Buildah, Skopeo, CRI-O, conmon, crun, Netavark, Quadlet — these are all neighbors in the same family. Reading one helps you read the others.
  • Configuration is spread across several files: containers.conf, storage.conf, registries.conf, policy.json. Each component reads its own. This is initially annoying and ultimately correct.

If you hold those three ideas in your head, you can predict Podman behaviors you’ve never seen. That’s the test.


6. The Architecture in Plain English

Let’s narrate what happens when you type podman run -d --name web -p 8080:80 nginx. End-to-end.

You hit Enter. The shell execs the podman binary. Podman parses your arguments and loads its configuration from ~/.config/containers/containers.conf and the system-wide /etc/containers/containers.conf. It then reaches into ~/.local/share/containers/storage/ and checks: do we already have the nginx image locally? Yes? Skip the pull. No? It uses the containers/image library to talk to docker.io, pulls the manifest, then pulls each layer it doesn’t already have. Layers are stored on disk under the overlay driver, which uses overlayfs to stack them into a unified filesystem when the container runs. (Or fuse-overlayfs if you’re rootless on an older kernel that doesn’t support unprivileged overlay mounts.)

With the image cached, Podman now needs to create the container. It generates an OCI bundle in ~/.local/share/containers/storage/overlay-containers/<id>/. The bundle is just a directory containing a config.json (the OCI runtime spec) and a rootfs (the assembled filesystem the container will see).

Now Podman enters the user namespace. It calls unshare (and friends) to create the namespace bundle the container will live in: a fresh PID namespace, fresh mount namespace, fresh network namespace, fresh user namespace mapping your host UID to UID 0 inside.

It then forks. The forked process execs conmon. Conmon is the babysitter. It double-forks to detach from Podman, opens pipes for the container’s stdio, and then execs the OCI runtime (crun) with the bundle path. crun reads config.json, applies the namespaces, the cgroups, the seccomp filter, the capability set, the mount points, and finally execs the container’s entrypoint (nginx). crun then exits — its job is done.

What’s left:

systemd (your user instance)
  └── conmon (PID 12345, running as you)
       └── nginx (PID 1 inside its namespace, PID 12346 on host, running as UID 100000)

For networking: because you’re rootless, pasta (or slirp4netns) was started as another userspace process. It owns one end of a TAP device in the container’s network namespace; the other end is a userspace socket. Traffic flows: container → kernel routes to TAP → pasta reads from TAP → pasta opens a host-side socket and forwards. The -p 8080:80 mapping is implemented by a small helper called rootlessport, which listens on host port 8080 (as your user) and forwards to the pasta-side endpoint of the container.

The podman process? Long gone. It exited several paragraphs ago, after writing the container’s metadata to disk and confirming that conmon had reported the container as started.

When you next type podman ps, the binary starts up again, reads ~/.local/share/containers/storage/, sees that container web is recorded as running, checks that its conmon’s PID is still alive (it is), reads its current state from a file conmon updates, and prints a row in the table. Then podman exits again.

This is the entire architecture. There is no central anything. The kernel holds the state. Conmon supervises. systemd (or your shell) is the parent. Podman is a CLI that knows how to talk to all of them.

Where state lives:

  • Image layers and container filesystems: ~/.local/share/containers/storage/ (rootless) or /var/lib/containers/storage/ (rootful).
  • Container metadata (names, IDs, restart counts): the same storage tree, in a BoltDB-backed catalog.
  • Live process state: in the kernel, in /proc, and in small files conmon writes to ~/.local/share/containers/storage/overlay-containers/<id>/userdata/.
  • Configuration: containers.conf, storage.conf, registries.conf, policy.json — system-wide in /etc/containers/ and per-user in ~/.config/containers/.

When something is broken, this is the geography you walk through.


7. The Things That Bite You

This section is gotchas — non-obvious behaviors that ship as bugs in your understanding for the first six months of using Podman. Each connects back to a mental model from Section 5.

1. Bind-mounted files have wrong ownership inside the container

You’d expect: A file you own on the host shows up owned by you in the container. What actually happens: The container has its own UID namespace. Your host UID 1000 maps to UID 0 inside (the container root). But files in your bind-mounted directory don’t get translated — they appear with their raw host UIDs, which inside the container look weird: a file owned by host UID 1000 appears owned by UID “nobody” (65534) or some high number. Why: Core Idea 2. User namespace mapping is one-way for processes, not for filesystem metadata. How to handle: For dev mounts where you want bidirectional ownership, use --userns=keep-id. For volumes the container manages exclusively, use named volumes (-v name:/path) and let Podman initialize ownership correctly. On SELinux systems, add :Z to bind mounts: -v ./src:/app:Z.

2. Privileged ports (<1024) refuse to bind

You’d expect: podman run -p 80:80 nginx works. What actually happens: Error: rootlessport cannot expose privileged port 80. Why: Core Idea 2. The kernel reserves ports below 1024 for processes with CAP_NET_BIND_SERVICE in the host namespace. Rootless Podman doesn’t have that capability on the host. The container thinks it can bind anything, but the host-side rootlessport helper can’t. How to handle: Lower the kernel floor with sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80 (or =443 for HTTPS), and persist it in /etc/sysctl.d/. Or use a higher port and front it with a reverse proxy. Or run rootful Podman for that specific container. Or use socket activation via systemd, which lets systemd hold the privileged port and hand it to your container.

3. Container-to-container DNS doesn’t work on the default network

You’d expect: podman run --name db ...; podman run --name app ... lets app ping db by name. What actually happens: It doesn’t. DNS resolution between containers only works on user-defined networks, not on the default rootless network. Why: The default rootless network uses pasta/slirp4netns in a mode that doesn’t include a DNS service. Named networks use Netavark, which sets up an embedded DNS server (aardvark-dns) for service discovery. How to handle: Always create a named network for any multi-container setup: podman network create mynet, then --network mynet on every container. This becomes muscle memory after one debugging session.

4. Rootless and rootful Podman have completely separate worlds

You’d expect: An image you pulled as your user is available when you sudo podman run it. What actually happens: It’s not. Rootful Podman stores images in /var/lib/containers/storage/, rootless in ~/.local/share/containers/storage/. They share nothing. Why: Different storage roots, different UID maps, different network backends. They’re effectively different Podman installations that happen to share a binary. How to handle: Pick one and stick with it. Use rootless by default. If you need rootful for a specific container, accept that everything (images, volumes, networks) for that container is in a parallel filesystem location. Don’t mix without a reason.

5. The first command after a reboot can be slow

You’d expect: podman ps is fast every time. What actually happens: The first invocation after a fresh boot can take a couple of seconds, especially with many containers. Why: Core Idea 1. There’s no warm daemon. Podman has to read state from disk, validate it against the kernel, and rebuild its in-memory picture every invocation. Subsequent invocations are fast because the OS has cached the relevant files. How to handle: This is structural. Accept it. The flip side — no daemon — is worth it. If sub-second ps matters operationally, you’re at a scale where you should be using Kubernetes anyway.

6. --restart=always doesn’t survive a reboot the way you think

You’d expect: podman run --restart always means the container is back after a reboot. What actually happens: It doesn’t, unless you’ve also done podman generate systemd / set up Quadlet / enabled podman-restart.service. Why: Core Idea 1 again. With no daemon, nothing is running at boot to bring containers back up. --restart only governs behavior while Podman/conmon is still around — i.e., if the container itself crashes, conmon restarts it. A full host reboot kills conmon and everything else. How to handle: For anything you want surviving a reboot, use Quadlet (.container files in ~/.config/containers/systemd/) and systemctl --user enable. For user services, also enable lingering: sudo loginctl enable-linger $USER, or your services won’t start at boot before you log in.

7. SELinux relabeling silently scrambles your data

You’d expect: -v ./data:/app:Z just makes the mount work. What actually happens: It recursively relabels every file in ./data with a container-private SELinux label. If ./data is /var/lib/postgresql and you do this, your host PostgreSQL stops working because its files now have the wrong labels. Why: SELinux enforces type-based access control. The :Z flag is “relabel this exclusively for this container,” which is correct for directories the container owns and destructive for directories shared with other host services. How to handle: Only use :Z on directories created specifically for the container. For shared directories, use lowercase :z (shared label) or, better, don’t bind-mount shared system directories at all — use a copy.

8. The Podman machine on macOS hides a network layer you’ll eventually trip over

You’d expect: A bind mount from /Users/you/code “just works” with native filesystem performance. What actually happens: Performance is decent but not native (it’s virtiofs or 9p, depending on backend), and certain operations (inotify, some mmap patterns) behave subtly differently than on Linux. Why: Your Mac is not Linux. The container runs inside a Linux VM. The bind mount crosses a hypervisor boundary. How to handle: For development that involves heavy filesystem watching (large Node.js projects, etc.), expect some friction. Consider running your dev work inside the VM rather than bind-mounting through. Or use volumes (which live in the VM) for hot paths and bind mounts only for code editing.

9. podman compose is not actually Podman — it’s Docker Compose talking to Podman’s socket

You’d expect: podman compose is a Podman-native implementation. What actually happens: In Podman 5+, podman compose shells out to the Docker Compose binary if it can find one. The legacy podman-compose (separate Python project) is its own thing, with its own bugs. Why: Compose is hard. Re-implementing it bug-for-bug was deemed a losing battle. Using Docker’s own binary against Podman’s REST API achieves compatibility cheaply. How to handle: Install docker-compose (the standalone binary or Compose v2 plugin) alongside Podman. podman compose will find and use it. If you must use podman-compose (the Python one), expect rough edges with depends_on: condition: service_healthy, GPU passthrough, and some networking modes.

10. Cgroups v1 vs v2 quietly limits which OCI runtime you can use

You’d expect: Podman just works on any modern Linux. What actually happens: Rootless Podman with cgroups v1 has severely limited resource control (no memory limits in rootless, for example). On cgroups v2, everything works correctly. Why: cgroups v1 doesn’t expose its controllers to unprivileged users. cgroups v2 does, via the systemd-managed delegation hierarchy. How to handle: Use a distribution shipped after 2020 (Fedora 31+, Debian 11+, Ubuntu 21.10+, RHEL 9+) — all default to cgroups v2. If you’re stuck on a v1 system, accept that rootless resource limits will be ignored and either run rootful or upgrade.


8. The Judgment Calls

These are the decisions an experienced Podman user makes without thinking, but that beginners agonize over or get wrong. Each is a real fork in the road.

1. Rootless vs Rootful

The question: Should this container run as my user, or under sudo as actual root? Rootless wins when: Almost always. It’s the default, it’s safer, it’s where the project’s effort is going, and 95% of workloads don’t need anything else. Rootful wins when: You need to bind a privileged port and can’t lower the kernel floor; you’re running an actual production network service where the container needs CAP_NET_ADMIN; you’re building images that require privileged operations like setcap during install; you’re using device passthrough that requires real device access (some GPU workloads). The experienced choice: Default rootless, with specific named exceptions. Don’t run “everything as rootful” because of one container that needs it. Maintain two named machines on Mac/Windows (podman machine init dev-rootless, podman machine init --rootful dev-rootful) so you can choose per workload without polluting your daily environment. The signal: If you find yourself adding sudo to most of your podman commands, you’ve drifted into rootful by accident. Stop and ask why.

2. Bind mount vs named volume

The question: Where should this container’s persistent data live? Bind mount wins when: You’re developing and want to edit the files with your host editor; you’re reading host-managed config files; you want the data backed up by your normal host backup tooling. Named volume wins when: The container owns the data exclusively; you don’t want to think about ownership and permissions; the data is a database, cache, or other format you’ll never touch directly. The experienced choice: Volumes for state Podman manages; bind mounts for code you edit. Mixing is fine. Avoid the antipattern of bind-mounting your entire home directory into a container “just in case.” The signal: If you’re chowning files inside the container to make things work, you should have used a named volume.

3. podman run vs Quadlet

The question: Should I just type podman run --restart always or set up a systemd unit? podman run wins when: Truly ephemeral, one-shot, dev/test usage. Anything you’ll kill in five minutes. Quadlet wins when: The container must survive reboots; you need proper logging via journalctl; you want dependency ordering with other services; you’re shipping this to a server where someone else might need to debug it. The experienced choice: Anything that runs for more than a day on a server should be a Quadlet. The cognitive overhead of a .container file is small and the operational payoff (real service management, proper logs, restart policies, dependency ordering) is large. podman run is for the REPL. The signal: If you have a postit note that says “remember to run podman run -d ... after rebooting” — you needed Quadlet a month ago.

4. Pod vs network

The question: I have multiple containers. Should they be in a Podman pod or just a shared named network? Pod wins when: Containers must share a network namespace (talk to each other via localhost); they have a tight lifecycle dependency (always start/stop together); you intend to run this on Kubernetes eventually. Network wins when: Containers are loosely coupled services that just need to discover each other; lifecycle is independent; one might be scaled or restarted independently of others. The experienced choice: Default to a named network for unrelated services. Use a pod only when there’s genuine namespace sharing (sidecar pattern, app + log shipper) or when you’re prototyping a Kubernetes workload locally. The signal: If your “pod” contains three unrelated services that just happened to be deployed together, it’s not a pod — it’s a shared network in disguise.

5. Compose vs kube play

The question: I have a multi-container app. Do I write Compose YAML or Kubernetes YAML? Compose wins when: Local dev only; the team knows Compose; the app is small; you want simple onboarding. Kubernetes YAML wins when: This app will eventually live on Kubernetes (so why use two different formats?); you want your local environment to match production; you need pod-level concepts that Compose doesn’t represent well. The experienced choice: For new projects in 2026, Kubernetes YAML via podman kube play. You learn one format, it works locally and in cluster, and the model maps cleanly to where production is going. Compose is fine for existing apps and for solo developers, but it’s a dead end if your career trajectory touches Kubernetes. The signal: If you find yourself writing two YAML files — one Compose for dev, one Kubernetes for prod — and trying to keep them in sync, you’re paying a tax you don’t have to pay.

6. Default network vs custom network

The question: Do I really need to create a network, or can I just podman run? Default network wins when: A single container, no need for inter-container DNS, simple port mapping. Custom network wins when: Anything else. Especially: multiple containers, or you ever want predictable DNS, or you ever want to control the subnet, or you want network isolation between groups of containers. The experienced choice: Custom network nearly always. The default is for one-off podman run alpine sh debugging. The signal: If you’re using container IPs (172.17.0.x) anywhere in your code or config, you’ve defaulted yourself into a corner. Names are the abstraction.

7. Building with Podman vs Buildah directly

The question: Should I use podman build or invoke Buildah? podman build wins when: You have a Dockerfile and want the standard build flow. Buildah wins when: You want to build images without a Dockerfile — scripting layers in shell, for example, for highly custom or optimized images. Or when you’re building inside a container where you don’t want a full Podman install. The experienced choice: podman build for the 95% case. Reach for raw Buildah when you’re doing something a Dockerfile can’t elegantly express, or when image size matters down to the kilobyte. The signal: If you find yourself writing Dockerfiles with twenty-line RUN commands held together with && to avoid extra layers, Buildah’s scripted approach is cleaner.

8. crun vs runc

The question: Which OCI runtime should I use? crun wins when: Modern Linux (cgroups v2); you care about startup latency; you care about memory overhead; you want newer kernel features faster. runc wins when: You’re stuck on an older system; you have specific compatibility needs with Kubernetes versions pinned to runc. The experienced choice: crun. crun is a much smaller binary. If compiled with -Os, the crun binary is ~300k. runc is currently ~15M. That means runc is about 50 times larger than crun. On a modern distro, Podman picks crun for you. Don’t override the default unless you have a specific reason. The signal: You almost never need to think about this. If you are, ask “why” and the answer is usually that you don’t actually need to.

9. Pulling from Docker Hub vs Quay.io vs Red Hat registry

The question: Where should I get my base images? Docker Hub wins when: You need the broadest selection; the official image is there; you’re not concerned about rate limits. Quay.io wins when: You’re already in the Red Hat ecosystem; you want non-anonymous registry access without per-pull rate limits; you want Red Hat’s verified images (UBI). Red Hat registry wins when: You’re running RHEL and want the Universal Base Image, which is supported by Red Hat for free even outside RHEL subscriptions. The experienced choice: Use UBI as a base when running on Red Hat infrastructure (it’s a supported, regularly patched base image). Use Alpine or Debian-slim from Docker Hub for everything else. Avoid pinning to :latest anywhere — pin to a specific tag and pin in Renovate or similar. The signal: If you’re hitting Docker Hub rate limits in CI, you should have a registry mirror or use Quay.

10. Podman Desktop vs CLI-only

The question: Should I install the GUI on my Mac/Windows machine? Desktop wins when: You’re new to containers; you want easy machine management; you want a Kubernetes (Kind/Minikube) integration baked in; multiple developers on your team have varying comfort with the CLI. CLI wins when: You’re comfortable in the terminal; you don’t need image-browsing UIs; you want minimal install footprint. The experienced choice: Install Podman Desktop on Mac/Windows even if you’re a CLI user — the machine management alone is worth it (start/stop/init from a menu bar). On Linux, skip it. The signal: If you find yourself running podman machine start from the terminal twenty times a week, Podman Desktop would have done that for you.


9. The Commands/APIs That Actually Matter

A grouped quick reference. Section 4 taught these in workflow context; this is what you reach for during real work.

Daily container ops

podman ps              # running containers
podman ps -a           # everything including stopped
podman logs -f <ct>    # tail logs (use -n 100 for last N lines)
podman exec -it <ct> sh   # shell into a running container
podman inspect <ct>    # full JSON dump of container state
podman top <ct>        # processes inside a container
podman stats           # live resource usage (htop for containers)
podman stop <ct>       # graceful
podman rm -f <ct>      # force delete

podman inspect is your friend. Almost every “why is this not working” question can be answered by reading the JSON. Pipe it through jq to find what you want: podman inspect web | jq '.[0].NetworkSettings'.

Images

podman pull <img>             # download
podman images                 # list local
podman rmi <img>              # remove one
podman image prune            # remove dangling
podman image prune -a         # remove all unused
podman save -o img.tar <img>  # export to tarball
podman load -i img.tar        # import from tarball
podman tag <img> newname:tag  # alias an image
podman push <img>             # to registry

The save/load pair is useful for air-gapped environments and for shipping an image to a colleague.

Networking

podman network ls                          # list networks
podman network create mynet                # create one
podman network create --subnet 10.89.0.0/24 mynet  # with explicit subnet
podman network inspect mynet               # details
podman network rm mynet                    # remove (must be unused)

For containers, --network mynet is the common flag. --network host skips namespacing entirely (container shares host network — only rootful, and rarely a good idea). --network none gives the container no networking.

Volumes

podman volume ls
podman volume create mydata
podman volume inspect mydata    # see where it lives on disk
podman volume rm mydata

Pods

podman pod create --name p1 -p 8080:80
podman pod ps
podman pod stop p1
podman pod rm p1
podman pod inspect p1
podman run --pod p1 ...   # add containers to pod

Kubernetes interop

podman kube generate <pod>     # write YAML for a running pod
podman kube generate <pod> > app.yaml
podman kube play app.yaml      # run pods/containers from YAML
podman kube down app.yaml      # tear them down

The YAML this generates is real Kubernetes YAML. You can kubectl apply it.

Systemd / Quadlet

# Modern: drop a .container file in ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user start mything.service
systemctl --user enable mything.service
journalctl --user -u mything.service -f    # tail logs

# Legacy (still works, no longer preferred):
podman generate systemd --name <ct> --files --new

For system-wide services (run by root or by a system user), put .container files in /etc/containers/systemd/ and drop the --user flag.

Machine (Mac/Windows)

podman machine init        # create
podman machine init --rootful  # rootful machine
podman machine start
podman machine stop
podman machine ssh         # shell into the VM
podman machine list
podman machine rm          # destroy

Diagnostics

podman info                    # comprehensive environment dump
podman info | grep -i rootless # confirm rootless mode
podman system df               # disk usage breakdown
podman system prune            # clean up everything unused
podman events                  # live stream of container events
podman unshare cat /proc/self/uid_map   # see your user namespace mapping

podman info is the universal first command when something is wrong. Half the answers are in its output.


10. How It Breaks

Failure mode 1: “Permission denied” on a bind mount

Symptoms: Container can’t read or write a host directory it has mounted. Root cause: Either UID mapping (Core Idea 2) — host UID doesn’t match expected container UID — or SELinux (the host directory has a label the container can’t access). Diagnose:

ls -ln /path/on/host        # what UID owns it on the host?
podman exec <ct> ls -ln /path/in/container   # what UID inside?
ls -lZ /path/on/host        # what SELinux label?
podman exec <ct> id         # what UID is the container running as?

Fix: Either --userns=keep-id to align UIDs, or use a named volume, or add :Z/:z for SELinux, or chown the directory to match the mapped UID range.

Failure mode 2: “rootlessport cannot expose privileged port”

Symptoms: -p 80:80 fails with this exact error. Root cause: Rootless can’t bind <1024 (Core Idea 2). Diagnose: Read the error. It tells you the kernel sysctl to change. Fix: sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80, persist in /etc/sysctl.d/. Or use port 8080 and reverse-proxy. Or run rootful.

Failure mode 3: Containers can’t reach each other by name

Symptoms: curl http://db:5432 from inside app fails with “name resolution failure.” Root cause: Using the default network, which lacks DNS service discovery. Diagnose: podman inspect <ct> | jq '.[0].NetworkSettings.Networks' — if it says podman (the default network), you’re in this state. Fix: Create a network: podman network create mynet. Re-create both containers with --network mynet. Names will now resolve.

Failure mode 4: “subuid/subgid not set”

Symptoms: podman run fails immediately with messages about missing subuid/subgid entries. Root cause: Your user doesn’t have a subuid range allocated. Newly created users on some distros lack this. Diagnose:

grep $USER /etc/subuid /etc/subgid

If empty, that’s the problem. Fix:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

Then podman system migrate to apply the change to existing containers.

Failure mode 5: Containers don’t survive reboot

Symptoms: --restart=always containers are gone after a reboot. Root cause: No daemon means nothing brings them back at boot (Core Idea 1). Diagnose: systemctl --user list-units | grep podman — if you don’t see your container as a unit, it’s not managed by systemd. Fix: Convert to Quadlet (drop a .container file in ~/.config/containers/systemd/). Also: sudo loginctl enable-linger $USER so your user services start at boot, not just at login.

Failure mode 6: Stale state after upgrades

Symptoms: Random weird errors after upgrading Podman or rebooting. “No such file or directory” referring to internal paths. Root cause: State left from a previous Podman version or a previous user namespace mapping. Diagnose: Errors mention paths under /run/user/$UID/containers/ or similar. Fix: podman system migrate (light) or, last resort, podman system reset (nuclear — destroys all your containers, images, volumes). The nuclear option fixes essentially everything but you lose your work.

General debugging workflow

When something is wrong and you don’t know where to start, run these in order:

  1. podman info — confirm the basics (rootless? cgroups v2? right runtime?)
  2. podman ps -a — what’s the container state actually?
  3. podman logs <ct> — what did it say before dying?
  4. podman inspect <ct> — full state, especially the Mounts, NetworkSettings, and Config.Env
  5. journalctl --user -u <ct>.service — if it’s a Quadlet, systemd has more
  6. podman events (in another shell) and reproduce — see exactly what the engine is doing

That sequence finds 90% of issues without Googling.


11. The Downsides / Disadvantages

A document that doesn’t tell you what Podman is genuinely bad at is selling, not teaching. Here is what you sign up for.

1. The macOS and Windows experience is a permanent second-class citizen.

Containers are Linux. Always have been. So on Mac and Windows, Podman runs a Linux VM (podman machine) and forwards your commands into it. That’s the same boat Docker Desktop is in. The difference is that Docker has spent a decade polishing this — file sharing performance, network forwarding quirks, the GUI integration — and Podman Desktop, while improving fast, has not had that decade.

Where it comes from: Podman was conceived as a Linux-native tool. Mac/Windows support was retrofitted. The VM is real, it’s a hop you can feel, and the bind-mount filesystem (virtiofs or 9p) is genuinely slower than native Linux. Podman Desktop is improving rapidly and provides a cleaner path for Linux-first teams who prefer open-source, daemonless tooling. However, on macOS and Windows it still relies on virtualized environments and lacks the refinement and ecosystem maturity of Docker Desktop.

What it costs: Slow bind mounts for large codebases, occasional VM resource quirks (the VM has a fixed RAM budget; if you give it 4GB and try to build a giant image, you OOM), and the cognitive overhead of a hidden VM. Newer Mac developers might not realize the VM exists and get confused by errors that are really VM errors.

Dealbreaker when: Your team is mostly on Macs doing heavy frontend dev with thousands of files watched by webpack. Native Docker Desktop or OrbStack will be a smoother experience.

Mitigation that’s actually denial: “It’s basically the same as Docker Desktop.” It isn’t. It’s close, but if you’ve ever had a file-watching problem in Docker Desktop on Mac and thought “this is annoying,” Podman Desktop is similarly annoying, with less institutional fixes catalogued in Stack Overflow.

2. The Compose ecosystem is borrowed, not native.

podman compose in modern versions delegates to the real Docker Compose binary. The legacy podman-compose is a separate Python project that approximates Compose v1/v2 behaviors. Not every Docker Compose feature translates to Podman Compose. Build contexts with complex arguments often fail or behave differently. Docker Compose plugins and extensions won’t work at all.

Where it comes from: The Podman team’s stated direction is Kubernetes YAML, not Compose. They support Compose for compatibility but it’s not where their effort goes.

What it costs: Some Compose features don’t work (advanced healthchecks, certain GPU passthrough, some networking modes, Compose plugins). Docs are thinner. Bug reports about Compose-specific behavior take longer to address because they’re at the seam between two projects.

Dealbreaker when: Your team’s entire local-dev workflow is docker-compose up with complex healthcheck dependencies, depends_on conditions, and Compose extension plugins. The migration cost may not be worth it.

Mitigation that’s actually denial: “We’ll just use podman-compose.” For real production use, you’ll either rewrite as Kubernetes YAML or end up running Docker Compose against Podman’s socket — which works, but at that point what are you really buying?

3. The single-host orchestration story stops at Quadlet.

Docker has Swarm. Podman has… systemd. That’s the orchestration story for multi-host. Finally, Podman’s support for advanced features such as orchestration, load balancing, and high availability is not as mature as Docker’s.

Where it comes from: Red Hat’s bet is “if you need multi-host, you need Kubernetes (or OpenShift).” There’s no in-between product. Podman is explicitly a single-host tool.

What it costs: If you have 5-20 hosts and want simple cross-host container orchestration without standing up a full Kubernetes cluster, your options are weak. You’ll end up with a hand-rolled Ansible setup or a tiny k3s cluster.

Dealbreaker when: You’re running a fleet of small Linux hosts (edge servers, lab machines, IoT-ish setups) and Kubernetes is genuinely too heavy. Docker Swarm — limited as it is — still fits a niche Podman doesn’t.

4. Documentation and Stack Overflow are thinner than Docker’s.

Type a Docker error into Google and there are six relevant answers from 2017 onwards. Type the same Podman error and you get 1-2, often newer and less battle-tested. Extensive documentation — Largest library of tutorials and Stack Overflow answers is on Docker’s side.

Where it comes from: Docker has 6+ years of community head-start. The Podman docs are good (Red Hat is a docs-first company) but the long tail of “weird user stories with weird fixes” is shallower.

What it costs: Time. When you hit something unusual, you may end up reading source code, GitHub issues, or asking on Matrix. For newer engineers, this is a real productivity tax.

Dealbreaker when: Your team is junior and you don’t have time to absorb the steeper search cost. Friction for new hires is real.

5. Rootless networking has a permanent performance ceiling.

User-mode networking via pasta or slirp4netns is in userspace. Every packet through the container’s network is translated by a user process, not by the kernel.

Where it comes from: Core Idea 2. Creating real network bridges and modifying iptables require host root. Rootless can’t have those. Pasta is faster than slirp4netns, but neither matches native kernel networking.

What it costs: Throughput is lower than rootful (often 20-50% lower for high-bandwidth scenarios), and latency is slightly higher. For a web service doing thousands of small requests per second, you might not notice. For a containerized iperf or a high-throughput stream processor, you’ll definitely notice.

Dealbreaker when: Network performance is in your critical path and you cannot run rootful. Pick something else (or run rootful — which gives up most of the security argument).

6. The split rootless/rootful storage is permanent friction.

Images you pull rootless aren’t available rootful, and vice versa. Volumes likewise. Networks likewise. They’re entirely separate worlds.

Where it comes from: Each user (including root) has its own storage tree. Storage is per-user by design.

What it costs: You’ll occasionally pull the same multi-gigabyte image twice. You’ll occasionally sudo podman ps and see nothing, because your containers aren’t rootful. New users get confused.

Dealbreaker when: Disk is precious and your workload genuinely needs both modes routinely.

7. Some workloads simply require root, and there’s no clean middle ground.

Containers that bind privileged ports, that need real device passthrough (some GPU setups, USB devices, hardware video acceleration), that load kernel modules, or that need CAP_SYS_ADMIN for legitimate reasons — these don’t fit rootless cleanly.

Where it comes from: The kernel guards these capabilities against the initial user namespace, not against namespace-internal “root.”

What it costs: For workloads in this category, you either accept rootful (losing most of the security story), or you architect around it (reverse proxies for ports, host-side helpers for devices). Architecting around it is real engineering effort.

Dealbreaker when: Your workload is mostly in this category. You’ll spend more time fighting Podman than benefiting from it.

8. CI/CD platforms still assume Docker by default.

GitHub Actions, GitLab CI, CircleCI, Jenkins, you name it — every CI platform’s documentation, official runners, and example pipelines assume Docker. Podman is a first-class option on some (GitLab, GitHub) and a manual setup on others.

Where it comes from: Inertia. Docker got there first, and CI platforms have not deprecated their Docker-default integrations.

What it costs: When you build a new CI pipeline, the path-of-least-resistance is still Docker. Choosing Podman means swimming against the documentation. Tools like Testcontainers have had Podman support for a while now but the “happy path” is still Docker.

Dealbreaker when: Your CI pipelines lean heavily on Docker-specific features or third-party Docker-socket-based integrations (Portainer, some scanners, some service-mesh dev tools).

9. The “Podman is identical to Docker” claim is mostly true and dangerous when it’s not.

The CLI is intentionally compatible. 95% of docker commands run identically as podman commands. But the 5% that don’t will bite you, and they don’t always fail loudly.

Where it comes from: Compatibility is a goal, not a guarantee. Behaviors around volumes (SELinux relabeling), networking (default network differences), and rootless quirks aren’t 1:1.

What it costs: Migrations seem easy, then surprise you. A Docker-fluent engineer can write a Podman script that “works” but does something subtly wrong (file ownership, SELinux labels, missing DNS). The aliasing pattern (alias docker=podman) is convenient but hides these differences from people who don’t realize the difference matters.

Dealbreaker when: Almost never. But know the risk.

10. Podman is fundamentally a Linux tool, and the project owns that.

Unlike Docker, which built itself into “the platform” across all developer OSes, Podman is upfront that Linux is the first-class target and everything else is a port. If you’re a heavy Mac user expecting parity, you’re going to be disappointed sometimes.

Where it comes from: Red Hat sells Linux. Podman’s funding is from Red Hat. The institutional alignment is unambiguous.

What it costs: Mac/Windows users feel like second-class citizens because, strategically, they are.

Dealbreaker when: Your team has zero Linux infrastructure and primarily works on Mac/Windows. Honestly, the OrbStack/Docker Desktop combination probably fits better.

None of these downsides invalidate Podman. They calibrate it. Podman is the right choice for a lot of teams. It is not the right choice for every team. The above is what you accept when you pick it.


12. The Taste Test

Here’s how to spot the experienced Podman user from the beginner. These are the patterns that show up in real codebases and infrastructure repos.

Bad: A README that says “install Docker”

The repo’s README says “install Docker and run docker compose up.” Then somewhere in the docs, a footnote: “Podman should also work.”

Good: A README that says “install Podman (or Docker)”

The README lists Podman first, mentions Quadlet for production deployment, and the Compose file works under podman compose with documented variations for SELinux systems.

Bad: podman run -d --restart always everywhere

Containers in production scripts started with bare podman run. No systemd, no Quadlet, no proper logging.

Good: A containers/systemd/ directory with .container files

Production deployments via Quadlet. Each service has a clear, declarative unit. Logs go through journald. Restart behavior is configured via systemd, not --restart.

Bad: Hardcoded container IPs

Code that talks to “the database at 172.17.0.2.” This breaks the moment the container restarts.

Good: Named networks with hostname-based discovery

# containers/systemd/db.container
[Container]
ContainerName=db
Network=app-net.network
# ...

App connects to db:5432. The network was explicitly defined as a .network Quadlet file.

Bad: Bind mounting /var/run/docker.sock for CI

A CI image that mounts the Docker socket to do “Docker in Docker” builds. Brittle, insecure, leaks build state.

Good: podman build directly in CI

Rootless Podman in CI, building images without a daemon socket. Buildah inside a CI container if even tighter isolation is needed. Running Podman, Buildah, and Skopeo inside containers is a common requirement for CI/CD pipelines on RHEL. The approach is cleaner than Docker-in-Docker because there is no daemon to manage.

Bad: A Dockerfile with USER root and --privileged

Built with assumptions about being root, run with --privileged to make up for whatever doesn’t work otherwise.

Good: A Containerfile (or Dockerfile) that drops privileges

USER 1001 near the top, no privileged operations after, runs rootless cleanly.

Bad: Volumes everywhere, ownership-by-trial-and-error

chown -R 1000:1000 /data scattered through scripts to make permissions work.

Good: --userns=keep-id for dev, named volumes for state

Dev bind mounts use --userns=keep-id so file ownership “just works.” Named volumes are used for state Podman manages — ownership is correct by construction.

Bad: Forgetting :Z on Fedora and writing it off as “broken”

A Fedora user installs Podman, tries a Docker-style bind mount, gets permission errors, and writes a blog post about Podman being broken.

Good: :Z understood and applied where appropriate

Bind mounts on SELinux systems use :Z for dedicated dirs and :z for shared. The user knows when not to use :Z (don’t relabel /var/lib/postgresql that the host PostgreSQL is using).

Bad: A single Podman machine on Mac, randomly rootful or rootless

podman machine init --rootful for “I needed it once” and forgotten about. Months later, debugging mysterious rootful behavior.

Good: Multiple named machines for different use cases

podman machine init dev (rootless), podman machine init build --rootful (for occasional privileged builds), explicit --connection flags or podman system connection default to switch.

Read a repo through this lens once and you’ll instantly see who in your team has actually used Podman in anger and who’s just typing the commands.


13. Where to Go Deeper

Five resources that are genuinely worth your time, in order of when to read them.

1. The official Podman documentation: https://docs.podman.io

What it’s good for: Reference. The command pages are well-maintained, and the architectural docs are honest about how things work. When to read it: As a reference when something specific isn’t behaving. Don’t try to read it cover-to-cover.

2. “Podman in Action” by Daniel Walsh

What it’s good for: Walsh is the originator of Podman and one of the most prolific Red Hat container engineers. His book is opinionated and connects design decisions to outcomes in a way few container books do. When to read it: After you’ve done the Distilled Introduction in this document and want one engineer’s full mental model.

3. Red Hat’s “Quadlet” introduction: https://www.redhat.com/en/blog/quadlet-podman

What it’s good for: The shortest path from “I run containers” to “I run containers as proper systemd services.” Quadlet is the production deployment story for Podman, and this article is the cleanest entry point. When to read it: Right after you’ve gotten comfortable with podman run, before you put anything Podman-shaped on a real server.

4. The “Shortcomings of Rootless Containers” GitHub document

What it’s good for: A maintained list of what rootless containers can’t do and why. Reading this honestly is what makes you respect-rather-than-fear Podman. Search containers/podman GitHub for “Shortcomings of Rootless Podman.” When to read it: When you start hitting your first rootless-specific wall and want to know if it’s an actual limit or a configuration miss.

5. The Podman source code on GitHub: https://github.com/containers/podman

What it’s good for: The gold standard when you’re stuck. The codebase is reasonably readable Go. The cmd/podman/ directory is where the CLI lives; libpod/ is the engine. Reading the source has saved me hours that docs and search couldn’t. When to read it: When you have a specific question that nobody else seems to have answered, and you need to know what the code actually does.

Skip:

  • Most Medium articles. They rehash the same surface comparison points with Docker and don’t go deep.
  • “10 Podman commands you should know” type listicles. They teach syntax; you don’t need that, you need understanding.
  • Old Stack Overflow answers about Podman v1/v2 (pre-2021). The tool has changed substantially.

14. The Final Verdict

Podman is what Docker would look like if Docker had been built by Linux engineers in 2019 instead of by macOS/web engineers in 2013. It is unmistakably a Red Hat tool, and that is a feature: it integrates with systemd, it respects SELinux, it defaults to least privilege, it cleaves cleanly into single-purpose components, and it treats Kubernetes as the obvious destination for anything that grows beyond a single host. If those words describe your environment, Podman is not just a Docker alternative — it is the correct tool, and you’ll feel friction every time you reach for anything else.

If those words describe almost nothing about your environment — if you’re a Mac developer working on a TypeScript app with twelve services in a docker-compose.yml, deployed to managed cloud containers — Podman will work, but it will feel like wearing a Linux server’s pajamas. The fit is imperfect, the polish in Docker Desktop is genuinely better, and the value of “no daemon” matters less when the daemon was never your problem.

What Podman gets profoundly right. First, the architectural integrity is real and rare: every behavior of the tool follows from a small set of design decisions, and those decisions reflect a coherent worldview about how Linux should work. Second, the rootless-by-default story is not just marketing — it is the security posture every container tool will eventually adopt, and Podman got there first by being willing to ship a rougher MVP and improve it. Third, the Quadlet integration with systemd is genuinely the right answer to “how do I run containers in production on a single host,” and nobody else has anything comparable.

What it gets wrong, or what it costs you. The Mac and Windows experience is fine but not great, and the project’s institutional incentives mean it will probably always be a step behind Docker Desktop in polish. The Compose story is a permanent borrow rather than a first-class implementation, and the orchestration story between “one host” and “full Kubernetes” is essentially missing. The split rootless/rootful worlds are confusing for newcomers, and rootless networking has performance ceilings you can’t engineer around. Documentation and community support are thinner than Docker’s, and they always will be — the Docker head-start is real.

Reach for Podman if you run Linux servers, especially Red Hat-family ones; you want rootless containers as a security baseline; you intend to deploy production workloads via systemd or Kubernetes; you’re operating in compliance-sensitive environments where audit teams care about root daemons; or you simply want your container tool to feel like a normal Linux citizen rather than a parallel universe.

Reach for something else if your team is mostly on Macs and lives in Docker Desktop’s polish; you’re heavily invested in Compose with advanced features; you need Docker Swarm-style lightweight multi-host orchestration; or you depend on Docker-socket-based ecosystem tools (Portainer, certain dev-mesh setups) without a Podman equivalent.

What you should believe after all this. Believe that “daemonless and rootless” are real engineering wins, not just talking points, and that they compound in production. Believe that Podman’s Kubernetes-shapedness is a feature, not a quirk — the conceptual continuity from podman pod to kubectl get pod is worth real money in onboarding and operations. Don’t believe the marketing claim that Podman is a “drop-in” Docker replacement; it’s drop-in 95% of the time, and the 5% will surprise you if you’ve leaned too hard on the equivalence. When someone tells you Podman is “just like Docker but better,” what they probably mean is “I work on Linux servers and the daemon was annoying me” — they’re right, and they’re projecting.

The honest line: containers are Linux processes wearing costumes, and Podman is the tool that admits it. Docker dresses the same Linux primitives in a daemon-shaped UX that worked great in 2013 and has been incrementally retrofitting around its original sins ever since. Podman started from “what if we just used the kernel directly, with normal Linux process management on top,” and the result is a tool that feels less impressive in a five-minute demo and considerably better at year three.

Pick the one that matches the shape of your problem. Just don’t pick by default.


The ideas are mine. The writing is AI assisted