TT Lab
Get started
Learn Learning paths Courses

Running Rootless Podman

What Actually Trips You Up in the Migration

Continue in TT Lab

In one line

Moving from docker to podman is not a swap of commands but a swap of the operating model. From a daemon to fork-exec, from rootful to rootless, and from daemon.json to the containers.conf family.

Why this was needed

With alias docker=podman, most commands work as they are. That makes the switch look easy. But when you actually migrate, you are bound to get caught on a few things. If you know those few in advance it takes 30 minutes; if you do not, half a day.

How it works

Differences in structure

Docker:  docker CLI ──REST──▶ dockerd ──▶ containerd ──▶ runc ──▶ 컨테이너
Podman:  podman CLI ──fork/exec──▶ conmon ──▶ crun ──▶ 컨테이너

podman has no daemon. podman run creates the container directly, like an ordinary process. A small watcher process, conmon, is attached to each container and manages stdio and the exit code, and the actual creation is done by the OCI runtime crun (written in C, lighter than runc).

The implications of this structure are large.

The five things you are bound to hit

1. restart: always behaves differently. With no daemon, it does not mean "start automatically at boot." The podman way is Quadlet.

# ~/.config/containers/systemd/web.container
[Unit]
Description=Web service

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

[Service]
Restart=always

[Install]
WantedBy=default.target

If you place a .container file, systemd reads it and generates a service unit. podman generate systemd has become outdated.

2. SELinux volume labels. On RHEL/Fedora, if a volume mount gives Permission denied, nine times out of ten it is SELinux. Attach the label -v ./data:/data:Z (private) or :z (shared). A compose file brought over from Ubuntu is where this trap hits most often.

3. Unqualified image names. podman pull nginx behaves differently from docker. Put unqualified-search-registries = ["docker.io"] in registries.conf, or, in CI, use the full path.

4. Storage location. Rootless images live in ~/.local/share/containers/storage. If you dig through /var/lib with the habits of docker system df, you will not find them.

5. Ports below 1024. Rootless cannot bind to privileged ports.

What about compose?

There are two ways.

If you are aiming at production, podman kube play (which runs Kubernetes YAML directly) is also an option instead of compose. It lets you unify the manifests for local and cluster.

What it looks like in the field

A gradual switch is possible. The two engines have separate storage, so they can coexist on one machine. You can move one service at a time.

The podman-docker package. It is a shim package that connects the docker command to podman. It is useful for script compatibility, but you must announce to teammates that "docker on this server is actually podman." Otherwise, when a command taken from the docker documentation behaves subtly differently, nobody can find the cause.

What you will do in the next lab

You load an image and run a container, check volumes and user mapping, reproduce the privileged port failure, and write a Quadlet .container file. At the end, you summarize the differences from docker in a table, with evidence.