# Podman, From Zero

> The daemonless, rootless container engine: a drop-in for most Docker commands, plus pods and the security win of running without a root daemon.


---

# Podman, From Zero

You already know Docker, and now a server, a CI runner, or a security review has put Podman in front of you. The good news: most of the muscle memory transfers. The interesting part is what changed underneath, because Podman threw out the one piece of Docker most people never think about until it bites them: the root daemon.

This guide gets you from "what even is this" to running real workloads, comfortably, in three phases.

## How to read this

If you know Docker, you can skim phase 2 for the deltas and spend your time on phases 1 and 3, where the real differences live. If you have never touched a container, read straight through; nothing here assumes Podman experience, only a willingness to type commands and read what comes back.

## The phases

1. [What Podman Actually Is](01-what-podman-actually-is.md) - the mental model: no daemon, no root, and why that matters.
2. [Running Containers and Pods](02-running-containers-and-pods.md) - the everyday commands, plus the one concept Docker doesn't have.
3. [Production Reality and Gotchas](03-production-reality-and-gotchas.md) - systemd units, rootless limits, and the places it differs.


---

# What Podman Actually Is

Here is the thing nobody tells you about Docker until you have to explain it to a security team: when you run `docker run`, your command barely does anything. It hands a request to a long-running background service - the Docker daemon, `dockerd` - and that service, running as root, does the actual work of pulling images and starting containers. Your terminal is a remote control. The daemon is the machine.

Podman's whole pitch is: get rid of the machine. There is no daemon. When you run `podman run`, *that process* pulls the image and starts the container. When the container runs, it is a child of your shell, not a child of a root service you never see. That one architectural choice ripples into everything else, so it is worth slowing down on.

## The daemon, and why it bothers people

Docker's architecture looks like this:

```text
you ──> docker (CLI) ──> dockerd (root daemon) ──> containerd ──> your container
        unprivileged     runs as root             also root      runs as root by default
```

*What just happened:* every container you start is ultimately launched by a privileged, always-on service. If `dockerd` crashes, every container it manages can go with it. If someone compromises the daemon's socket, they effectively have root on the host, because the daemon *is* root and will do what it is told.

Podman's architecture is flatter:

```text
you ──> podman ──> conmon ──> your container
        your user            your user (rootless)
```

*What just happened:* there is no central privileged broker. `podman` does the setup itself and then hands the running container to a tiny monitor process called `conmon` (container monitor), which babysits it so the container keeps running after your `podman` command exits. No root daemon sits in the middle.

> The shorthand you will hear is "daemonless." It does not mean nothing runs in the background - `conmon` does, one per container. It means there is no single long-lived root service that owns every container on the box.

## Rootless: the part that actually matters

"Daemonless" is the architecture. "Rootless" is the consequence people care about.

Because Podman runs as *you*, it can start containers without root at all. It does this with a Linux kernel feature called **user namespaces**: inside the container, a process can think it is UID 0 (root), while outside, on the host, it is mapped to your unprivileged user. The container sees root; the host sees you.

```console
$ id -u
1000

$ podman run --rm alpine id
uid=0(root) gid=0(root) groups=0(root),...

$ podman run --rm alpine sleep 600 &
$ ps -o user,pid,cmd -C sleep
USER       PID CMD
chris    48213 sleep 600
```

*What just happened:* inside the container the process is `root` (uid 0). On the host, the very same process belongs to `chris`, an ordinary user. A container breakout would land an attacker as `chris`, not as host root. That is the security win in one sentence: a compromised container does not hand over the whole machine.

This mapping is driven by two files, `/etc/subuid` and `/etc/subgid`, which grant your user a range of "subordinate" UIDs to hand out inside containers. If Podman ever complains about user namespaces on a fresh box, those files (or the `newuidmap`/`newgidmap` helpers) are almost always the missing piece.

## "Drop-in for Docker" - true, with footnotes

Podman implements the same command-line interface as Docker on purpose. The verbs, flags, and image format (OCI - the Open Container Initiative standard, which Docker also produces) are shared. So this works:

```console
$ alias docker=podman
$ docker run --rm -p 8080:80 docker.io/library/nginx
```

*What just happened:* you aliased `docker` to `podman` and a standard `docker run` line worked unchanged. Most scripts, most tutorials, most muscle memory carry straight over. Note the full image name `docker.io/library/nginx` - Podman does not assume Docker Hub the way Docker does, a difference phase 2 returns to.

The footnotes are real, though, and they all trace back to the missing daemon:

- There is no `docker.sock` by default, so tools that talk to the Docker API need Podman's compatibility socket switched on (phase 3).
- Containers do not auto-restart on reboot via a daemon, because there is no daemon to do it - you use systemd instead (phase 3).
- Rootless networking and low ports behave differently, because you are not root (phase 2 and 3).

None of these are dealbreakers. They are the bill that comes due for not running a privileged daemon, and for many teams it is a bill worth paying.

## For builders

If you are coming from [/guides/docker-without-the-magic](/guides/docker-without-the-magic), the mental shift is small but load-bearing: stop thinking of "the container engine" as a service you talk to, and start thinking of it as a command you run. Everything that used to be the daemon's job - restarts, the API socket, owning the container lifecycle - becomes either your job or systemd's job. That reframing is most of what makes Podman click.

```quiz
[
  {
    "q": "What does \"daemonless\" mean in Podman's case?",
    "choices": [
      "Nothing ever runs in the background",
      "There is no single long-lived root service that owns every container; each container has a small conmon monitor instead",
      "Containers cannot run after the terminal closes",
      "Podman cannot pull images without an internet daemon"
    ],
    "answer": 1,
    "explain": "Podman has no central root daemon; conmon (one per container) keeps each container alive, but nothing privileged brokers them all."
  },
  {
    "q": "In a rootless Podman container, a process reports uid 0. What is it on the host?",
    "choices": [
      "Also uid 0 (real root on the host)",
      "A mapped unprivileged user, via user namespaces",
      "A random uid that changes each second",
      "It has no host identity at all"
    ],
    "answer": 1,
    "explain": "User namespaces map container root to your unprivileged host user, so a breakout lands as you, not as host root."
  },
  {
    "q": "Why does `alias docker=podman` work for most commands?",
    "choices": [
      "Podman bundles a copy of dockerd internally",
      "Podman deliberately implements the same CLI and uses the same OCI image format",
      "The alias secretly installs Docker first",
      "Podman translates each command over the network to a Docker server"
    ],
    "answer": 1,
    "explain": "Podman shares Docker's command-line interface and the OCI image standard on purpose, so most usage transfers directly."
  }
]
```


---

# Running Containers and Pods

You came here to run things, so let's run things. This phase is the everyday loop - pull, run, inspect, clean up - and then the one genuinely new idea Podman gives you that Docker does not: pods.

## The commands you already know

If you have used Docker, this section is mostly confirmation. The verbs are the same.

```console
$ podman pull docker.io/library/redis:7
$ podman run -d --name cache -p 6379:6379 docker.io/library/redis:7
8f3a...
$ podman ps
CONTAINER ID  IMAGE                          COMMAND     STATUS         PORTS                   NAMES
8f3a1c2d4e5f  docker.io/library/redis:7      redis-...   Up 4 seconds   0.0.0.0:6379->6379/tcp  cache
$ podman logs cache
$ podman exec -it cache redis-cli ping
PONG
$ podman stop cache && podman rm cache
```

*What just happened:* a full pull-run-inspect-exec-teardown loop, identical to Docker except for the binary name. `podman ps`, `logs`, `exec`, `stop`, `rm`, `images`, `rmi`, `build` - all behave as you expect.

Two small differences will catch you the first day:

**Image names are not guessed.** Docker silently expands `redis` to `docker.io/library/redis`. Podman, by default, asks which registry you mean, or you spell it out:

```console
$ podman run redis
? Please select an image:
  ▸ registry.fedoraproject.org/redis:latest
    registry.access.redhat.com/redis:latest
    docker.io/library/redis:latest
```

*What just happened:* Podman refused to assume Docker Hub and offered the registries from your `registries.conf`. This is deliberate - implicit Docker Hub is how typo-squatting attacks land. Type the fully qualified name (`docker.io/library/redis:7`) in scripts and you will never see this prompt.

**`podman ps` is per-user.** Because there is no shared daemon, you only see *your* containers. Another user's rootless containers, and root's containers, are invisible to you. `sudo podman ps` shows root's set, which is a different world entirely (different storage, different images). Pick one mode per workload and stay in it.

## Pods: the idea Docker doesn't have

Here is where Podman earns its name. A **pod** is a group of containers that share a network namespace - meaning they share an IP address and can reach each other over `localhost`. If "pod" rings a Kubernetes bell, that is exactly the point: Podman borrowed the concept, so a pod you design locally maps cleanly onto how Kubernetes thinks.

Why would you want this? The classic case is an app plus a sidecar - a web server and a metrics exporter, or an app and a local cache - that should live and die together and talk over `localhost` with no networking ceremony.

```console
$ podman pod create --name web --publish 8080:80
$ podman run -d --pod web --name app docker.io/library/nginx
$ podman run -d --pod web --name sidecar docker.io/library/redis:7
$ podman pod ps
POD ID        NAME    STATUS    INFRA ID      # OF CONTAINERS
a1b2c3d4e5f6  web     Running   0f9e8d7c6b5a  3
```

*What just happened:* you created a pod, published the port **on the pod**, and added two containers to it. Notice the pod reports **3** containers, not 2 - Podman quietly adds an "infra" container that holds the shared namespaces open, so individual containers can come and go without tearing down the pod's network. The `app` and `sidecar` containers now share one IP; `app` can reach Redis at `localhost:6379` with no `--link`, no user-defined network, nothing.

A subtle but important detail: once a container is in a pod, you publish ports on the *pod*, not the container. The pod owns the network namespace, so a `-p` on an in-pod `podman run` is ignored. Set every port you need at `podman pod create` time.

Tearing a pod down is one command:

```console
$ podman pod stop web && podman pod rm web
```

*What just happened:* the pod, its infra container, and both app containers stopped and were removed together. Lifecycle as a unit - that is the entire value proposition of a pod.

## From a pod to Kubernetes

Because a pod is shaped like a Kubernetes pod, Podman can write the YAML for you:

```console
$ podman kube generate web > web.yaml
$ podman kube play web.yaml
```

*What just happened:* `kube generate` serialized your running pod into a Kubernetes manifest, and `kube play` can recreate it - locally in Podman, or as a starting point for a real cluster. It is not a magic "ship to prod" button (resource limits, real volumes, and probes still need your attention), but it is a genuine bridge from "works on my machine" to a manifest you can hand to [/guides/kubernetes-without-the-hype](/guides/kubernetes-without-the-hype).

> Treat `kube generate` output as a first draft, not a deliverable. It captures what you ran, not what production needs. Read every line before you apply it to a cluster.

## In the wild

A common rootless workflow: run your dev stack as a pod so the app and its database share `localhost` exactly like they would inside a single Kubernetes pod, then `kube generate` to seed the manifest your platform team will harden. You get Docker-grade local ergonomics and a Kubernetes-shaped artifact out the other end, without ever running a privileged daemon.

```quiz
[
  {
    "q": "You run `podman pod create --name web` then add two containers. Why does `podman pod ps` show 3 containers?",
    "choices": [
      "It double-counts the first container",
      "Podman adds an infra container that holds the shared namespaces open",
      "One container is a hidden logging agent",
      "It counts the pod itself as a container"
    ],
    "answer": 1,
    "explain": "Each pod gets an infra container that keeps the shared network namespace alive so member containers can come and go."
  },
  {
    "q": "Where should you publish a port for a container that lives inside a pod?",
    "choices": [
      "With -p on the container's `podman run`",
      "On the pod, at `podman pod create` time",
      "Ports cannot be published from pods",
      "In a separate `podman network` command only"
    ],
    "answer": 1,
    "explain": "The pod owns the network namespace, so a -p on an in-pod container is ignored; publish ports at pod-create time."
  },
  {
    "q": "Why does `podman run redis` sometimes prompt you to pick a registry, when Docker would not?",
    "choices": [
      "Podman cannot reach Docker Hub",
      "Podman deliberately refuses to assume a registry, to avoid typo-squatting from implicit Docker Hub",
      "The image name is invalid",
      "It is asking which architecture to download"
    ],
    "answer": 1,
    "explain": "Podman won't silently expand short names to Docker Hub; spell out the fully qualified name to skip the prompt."
  }
]
```


---

# Production Reality and Gotchas

Everything works on your laptop. Then you put it on a server, reboot, and your container does not come back - because there is no daemon to bring it back. This phase is the bill that comes due for going daemonless, and the well-worn answers to each item.

## Restarts: systemd is the daemon now

With Docker, `--restart=always` works because `dockerd` is always running and re-launches the container after a reboot. Podman has no such service, so the job falls to the init system every Linux server already runs: systemd.

The modern way is **Quadlet** - you describe a container in a small unit-like file, and systemd generates the service for you:

```ini
# ~/.config/containers/systemd/cache.container
[Container]
Image=docker.io/library/redis:7
PublishPort=6379:6379

[Install]
WantedBy=default.target
```

```console
$ systemctl --user daemon-reload
$ systemctl --user start cache.service
$ loginctl enable-linger $USER
```

*What just happened:* you declared a container in a `.container` file, systemd turned it into `cache.service`, and you started it as a *user* service - no root involved. The `enable-linger` line is the one people forget: by default a rootless user's services stop when you log out, so linger tells systemd to keep your user manager running across logout and reboot. Without it, your "always-on" container quietly dies the moment you disconnect.

> Older guides use `podman generate systemd` to emit a unit file you save by hand. It still works, but it is deprecated in favor of Quadlet. If you are starting fresh, write a `.container` file; if you inherit generated units, they will keep running.

## Rootless can't bind low ports

Linux reserves ports below 1024 for root. Rootless Podman is, by definition, not root, so this fails:

```console
$ podman run -p 80:80 docker.io/library/nginx
Error: rootless cannot bind to port 80: permission denied
```

*What just happened:* the kernel refused, because binding port 80 needs privilege you deliberately gave up. You have three straightforward options, in rough order of preference:

```console
# 1. Publish to a high host port, front it with a reverse proxy:
$ podman run -p 8080:80 docker.io/library/nginx

# 2. Lower the unprivileged-port floor system-wide (host change):
$ sudo sysctl net.ipv4.ip_unprivileged_port_start=80

# 3. Only if you truly need it: run that workload rootful (sudo podman).
```

*What just happened:* option 1 keeps the security model intact and is what most production setups do. Option 2 trades a little host hardening for convenience. Option 3 throws away the rootless win, so reach for it only when nothing else fits.

## The Docker API socket

Tools like `docker compose`, Testcontainers, or anything that talks to `/var/run/docker.sock` expect a daemon API. Podman can speak that API, but the socket is off until you ask for it:

```console
$ systemctl --user enable --now podman.socket
$ export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
$ docker-compose up    # now talks to Podman
```

*What just happened:* you started Podman's compatibility socket and pointed `DOCKER_HOST` at it, so Docker-API clients reach Podman instead. It is a faithful subset, not a perfect clone - most Compose files run fine; the rare feature that depends on a Docker-only API quirk will not. Note this socket is on-demand: it activates when a client connects and is not a permanent root daemon.

## Volumes, SELinux, and the `:z` you'll forget

On SELinux systems (Fedora, RHEL, and relatives - common Podman territory), a host bind-mount needs a label or the container cannot read it:

```console
$ podman run -v ./data:/data:Z docker.io/library/postgres:16
```

*What just happened:* the `:Z` told Podman to relabel `./data` so the container's SELinux context may access it (lowercase `:z` shares the label between multiple containers; uppercase `:Z` makes it private). Skip it on an SELinux host and you get baffling "permission denied" errors on files that obviously exist. This bites Docker users too, but Podman's typical homes turn SELinux on by default, so you meet it sooner.

## When to reach for Podman

Be clear-eyed about the tradeoff so you choose on purpose, not on hype:

- **Reach for it** when rootless is a security requirement, when you cannot or will not run a root daemon, when you are already on systemd and want first-class integration, or when you want pods that mirror Kubernetes locally.
- **Stay on Docker** when a critical tool only speaks the real Docker API, when your team's entire toolchain assumes `dockerd`, or when Docker Desktop's polish is worth more to you than the daemonless model. The migration cost is real even when it is small.

Neither is a moral choice. They produce the same OCI images and run the same workloads. Podman trades a little convenience for a meaningfully smaller attack surface, and now you know exactly where that trade shows up.

## In the wild

A typical server deployment ends up as: a Quadlet `.container` file per service, `enable-linger` set so they survive reboots, a high host port behind nginx or Caddy, and `DOCKER_HOST` exported only on the CI box that runs Compose-based integration tests. No root daemon anywhere - which is the entire reason the security team signed off.

```quiz
[
  {
    "q": "Your rootless Podman container does not come back after a server reboot. What is the modern fix?",
    "choices": [
      "Add --restart=always to podman run",
      "Define it with a Quadlet .container file as a systemd user service, and enable-linger",
      "Install dockerd alongside Podman",
      "Run podman ps in a cron job"
    ],
    "answer": 1,
    "explain": "There is no daemon to restart it; systemd is the daemon now. Quadlet plus enable-linger keeps a rootless service alive across reboots."
  },
  {
    "q": "Why does `loginctl enable-linger $USER` matter for a rootless always-on container?",
    "choices": [
      "It grants the user root for port 80",
      "Without it, the user's systemd services stop at logout, so the container dies when you disconnect",
      "It speeds up image pulls",
      "It enables the Docker compatibility socket"
    ],
    "answer": 1,
    "explain": "Rootless user services normally stop at logout; linger keeps the user manager running across logout and reboot."
  },
  {
    "q": "On an SELinux host, a bind-mount gives \"permission denied\" on files that clearly exist. What is usually missing?",
    "choices": [
      "The image tag",
      "A :z or :Z relabel suffix on the -v mount",
      "The podman.socket service",
      "An entry in /etc/subuid"
    ],
    "answer": 1,
    "explain": "SELinux blocks the container until the mount is relabeled; :z shares the label, :Z makes it private to that container."
  }
]
```
