Choose a container runtime in 2026 and the question underneath the brand names is architectural, not functional, because the two tools run the same images, honor the same specifications and produce the same running processes. The real fork is whether the fleet wants a long-lived service holding root, the Docker model, or a service-free command-line tool where every container belongs to the user who started it and root is optional, the Podman model. This article is the decision written out: what each runtime is, what rootless actually means mechanically, the systemd-native pattern that gives Podman its production shape on RHEL 9, the honest limits of the rootless world, and the migration path between the two, because most teams will run one of each at some point and the switch is smaller than it looks.

The shared foundation that makes this a fair comparison

Both runtimes sit on the same three standards. The OCI image specification defines what an image is, layers and manifests, so anything built by one tool runs under the other without conversion. The OCI runtime specification defines what a running container is, a process tree in a namespace-and-cgroups cage, and both tools delegate the actual caging to the same low-level runtimes, runc for Docker and crun for Podman by default, which are interchangeable. The OCI distribution specification defines how registries speak, so the same images pull from the same registries under both tools. The practical consequence deserves its own sentence: a container is not a Docker artifact or a Podman artifact, it is an OCI artifact, and the runtime choice is about the management layer above it.

The differences start at the process model. Docker runs a service, dockerd, started as root, that receives commands from the client, owns every container and socket, and mediates every operation. Podman has no service: each command forks the container process directly, the container belongs to the invoking user, and there is no central process to talk to, compromise or restart. The service-free model names exactly this, and the security argument that follows from it is structural rather than incidental: a service running as root with a command socket is a single high-value target on every host, while a service-free tool removes the target from the design, not from the hardening checklist.

What rootless containers actually are

Rootless is the headline Podman capability, and the mechanics matter because they define both the power and the limits. A rootless container runs without any root privileges, which is made possible by user namespaces, the kernel feature that maps a range of user IDs on the host to a different range inside the container, letting an ordinary user appear as root inside the cage while holding no privileges outside it. The ranges come from two files, /etc/subuid and /etc/subgid, which assign each user thousands of subordinate IDs, and the standard setup tooling, shadow-utils with newuidmap, makes the assignment a one-time affair:

usermod --add-subuids 200000-265535 --add-subgids 200000-265535 deploy

Inside the container, the process sees itself as uid 0 with full root powers over the container's filesystem and process tree, while the host sees the whole activity confined to the subordinate range, where the worst possible outcome is bounded by what the ordinary user could already do anyway. The shift to rootless changes the blast radius of a container escape from full host compromise to a user-level incident, and that single sentence is the entire security argument.

The honest limits are a short list, and production plans around them. Ports below 1024 need either the sysctl that lowers the unprivileged floor, net.ipv4.ip_unprivileged_port_start=80, or a front-end service binding the low port and forwarding, because an unprivileged process cannot bind them directly. Certain mounts, kernel filesystems and most device nodes, are unavailable or restricted rootless. And the cgroup hierarchy requires the unified v2 layout, which every current RHEL 9 and Debian-family release ships by default, the one legacy environment where rootless needs checking before promising.

The systemd-native production pattern on RHEL 9

RHEL 9 ships Podman as the container runtime of the platform, and the production pattern on it treats containers as systemd services rather than as long foreground processes. The native mechanism is the quadlet, a unit file format that declares a container directly in systemd syntax, a file under /etc/containers/systemd/ ending in .container:

[Unit]
Description=Web application

[Container]
Image=registry.example.com/app:1.4
PublishPort=8080:8080
EnvironmentFile=/etc/app/env
Volume=/srv/app/config:/config:ro,Z

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

a unit definition reload through systemctl makes the service appear, systemctl start app.service runs it, and the container's lifecycle is now the service's lifecycle, with restart policy, dependencies and logging in the same systemd language the rest of the server speaks. The pattern ends the historical awkwardness of containers-as-processes, where a reboot or a crash required an external supervisor, and the older bridge command, podman generate systemd --new, remains for fleets that have not moved to quadlets yet, generating the same integration from an existing container.

The compose story completes the production picture. Docker Compose files drive Podman through the compatibility layer, either podman-compose, which translates the compose specification natively, or the original compose tool against the API socket, enabled with systemctl enable --now podman.socket, which makes the Docker-compatible endpoint available for tools that expect it. Multi-container applications run under either path, and the choice between them is a tooling preference rather than a capability difference.

Migration between the runtimes

Moving a workload between the tools is deliberately boring, because the OCI foundation makes it a data and configuration exercise rather than a re-engineering project. Images migrate by pulling them under the new tool from the same registry, or by loading saved archives, podman pull and podman load mirroring docker pull and docker load. Volumes migrate by copying the data between the two storage roots, /var/lib/docker on the Docker side and /var/lib/containers with the per-user tree under ~/.local/share/containers for rootless Podman, a straightforward rsync with the destination's ownership adjusted to the subuid range where it applies.

The operational muscle memory transfers through the alias bridge, because the Podman command set was designed as a drop-in: docker run, docker ps and docker logs work identically as podman run, podman ps and podman logs, arguments and flags included, and the alias docker=podman on a workstation is the standard transition device. Scripts that call the docker binary run unchanged against the compatibility socket, and the CI pipeline that builds with Docker builds identically with Podman, since the build layer, Buildah under the Podman command, reads the same Dockerfiles and produces the same OCI images.

The decision framework

The choice that fits a team can be written as four questions. Is the fleet on RHEL 9 or a close relative, where Podman is the platform-supported path with quadlets and security policy in the same ecosystem? Rootless becomes the default answer for every service that does not need privileged mounts. Does the team's tooling center on the Docker API and Docker Compose as a product, with the socket as a first-class citizen? Docker remains a mature, unproblematic choice, and the service risk is managed with the same hardening stack this series built in its security articles. Does the architecture need a single registry-auth and socket story shared across mixed users? The socket model, either runtime's, defines that pattern. And is the container a long-running service on a systemd host? Quadlets make the answer Podman-flavored on any distribution that ships them.

A mixed verdict is a legitimate outcome and a common one, because the two tools answer different emphases rather than different problems: Docker remains the lingua franca of development laptops and CI pipelines, Podman carries the production hosts where rootless and systemd integration decide. The migration path between them is the safety net that makes the verdict reversible, and the OCI foundation underneath means the images, the real assets, never take a side at all.
## The build story, images from Dockerfiles or from scratch

Building images works identically in feel and differently in plumbing, and the differences reward a short look. Podman build delegates to Buildah, the same project's build engine, which reads standard Dockerfiles, multi-stage builds, ARG and ENV handling included, and produces OCI images compatible with every registry. The command is the familiar one, podman build -t app:1.4 ., the Dockerfile is unchanged, and the CI pipeline that switched runtimes keeps its build stage untouched.

The genuinely different capability is the Buildah script mode, where images assemble from shell commands rather than from a Dockerfile:

buildah from alpine:3.20 # buildah run container-name apk add --no-cache postgresql-client # buildah config --entrypoint /usr/bin/pgcheck container-name # buildah commit container-name pgcheck:1.0

The script form earns its keep in the specific place where Dockerfiles get awkward, builds that need conditionals, error handling or decisions based on artifacts, and a CI pipeline that builds this way reads as a program rather than as a recipe. Both forms produce the same image format, and the choice is per-project rather than per-fleet.

One rootless nuance completes the build story: builds without privileges use the fuse-overlayfs storage driver, which is transparent to the build script and slower by a small, measurable margin than the kernel overlay that privileged builds use. On CI runners, the margin matters at fleet scale and the standard answers are the privileged runner pool for heavy builds or the build farm that runs them at the pace of the kernel driver, while on laptops and ordinary servers the fuse-overlayfs cost is well inside the territory nobody notices.

A day of podman on a production host

The command vocabulary in daily use is where the drop-in promise becomes concrete. The morning check is podman ps -a, every container with its state, the same flag grammar as its counterpart. The log tail is podman logs -f app, the shell inside is podman exec -it app /bin/sh, and the resource ceiling comes from podman stats with no flags. Pulling the new version before the switch is podman pull registry.example.com/app:1.5, and the rollback image is still local when the update runs, which is the quiet property that makes container rollbacks the fastest recovery action in any runbook.

The rootless detail that shapes daily work is the location of everything: a rootless user's images, volumes and container state live under the home directory, ~/.local/share/containers, so the ordinary disk-quota tools manage container storage the same way they manage any user data, and a full home partition is a user-level event rather than a host-level outage. Rootful use, where the service-free tool still runs containers as root, keeps the state under /var/lib/containers and the two trees never mix, which is the design detail that makes mixed hosts, rootless applications and one rootful database, behave predictably for disk planning.

Registries round out the day: podman login registry.example.com with the same credentials file format, ~/.config/containers/auth.json, that the Docker tooling writes, so a copied login file moves the entire registry relationship between the tools. Skopeo, the sibling utility for image inspection and copy without a runtime, handles the fleet operations, copying images between registries, inspecting manifests, and syncing the approved image set to the internal mirror, the batch work that the per-host runtimes were never meant to do.