Podman vs Docker (2026): Rootless, Daemonless & Compose Tested

Podman vs Docker (2026): Rootless, Daemonless & Compose Tested

Podman vs Docker in 2026: Rootless, Daemonless, and Compose Tested

The podman vs docker debate stopped being a hobbyist argument the moment two forces collided in production: a long-running root daemon that widens your attack surface, and a licensing model that now bills mid-size companies for the desktop tooling their engineers reach for by reflex. Both pressures point at the same architectural question — do you actually need an always-on privileged process supervising every container on the box? For a growing share of teams, the honest answer is no, and that answer has a name.

This is not a “Docker is dead” piece. Docker Engine is still the most battle-tested OCI runtime on the planet, and its build cache and ecosystem depth are real advantages. But Podman has closed the workflow gap far enough that the choice is now a genuine trade-off analysis rather than a leap of faith. If you run rootless workloads, care about your host’s blast radius, or want containers supervised by systemd instead of a bespoke daemon, you owe yourself a clear-eyed comparison.

What this covers: the daemon-versus-fork-exec architecture, rootless user namespaces, pods and Quadlet, Compose and BuildKit compatibility, rootless networking, CI behavior, a decision matrix, the gotchas that bite during migration, and a straight recommendation.

Context and Background

Containers are standardized in a way that makes this comparison possible at all. The Open Container Initiative defines the image format and the runtime spec, so an image built by one tool runs under another without translation. When people say “Docker image,” they mean an OCI image; Podman consumes and produces the same artifacts. That shared substrate is why you can swap engines without rebuilding your registry or your CI pipeline from scratch — the disagreement is entirely about the process model that surrounds the runtime, not the containers themselves.

The word “Docker” is overloaded, and untangling it is the first step to a fair comparison. Docker is really three things bundled together: Docker Engine (the dockerd daemon plus containerd and the runc runtime), the Docker CLI (the docker client that talks to the daemon over a socket), and Docker Desktop (the packaged macOS/Windows/Linux application that wraps the engine with a VM, a GUI, Kubernetes, and update tooling). The 2021 licensing change — and the 2026 pricing that followed it — applies specifically to Docker Desktop, not to the open-source Engine. That distinction matters enormously, because a lot of “we have to leave Docker” conversations are really “we have to stop paying for Docker Desktop,” and those have different solutions.

The daemon model itself is the historical heart of Docker. Every docker run you type is a request to dockerd, a persistent process that traditionally runs as root and owns the lifecycle of every container, image, and volume on the host. That design gave Docker its famous ergonomics — a single control plane, a clean REST API, effortless docker ps across everything — but it also created a single privileged process that, if compromised, hands an attacker the keys to the machine. Podman was built by Red Hat’s container team specifically to keep the ergonomics and delete the daemon. If you are weighing runtimes at the orchestration layer too, our breakdown of containerd vs CRI-O for Kubernetes covers the same fork-exec-versus-daemon tension one level down the stack.

Framing the podman vs docker question this way — as Engine-plus-CLI-plus-Desktop versus a daemonless engine and a compatible CLI — keeps the comparison honest. You are not comparing two monoliths; you are comparing two process models that both terminate in the same runc-or-crun runtime and the same OCI image. That is why so much of what follows is about the surrounding machinery — who supervises the container, who holds root, who owns the socket — rather than about the containers themselves, which are effectively identical on both sides.

Architecture: Daemon vs Fork-Exec

The core difference is structural, not cosmetic. Docker uses a client-daemon architecture where a long-lived root process brokers every operation, while Podman uses a daemonless fork-exec model in which the CLI launches containers as direct child processes and then gets out of the way. Everything else — rootless security, systemd integration, the failure modes — flows from that one decision.

Docker routes commands through a persistent root daemon while Podman forks containers directly from the CLI

Figure 1: Docker’s docker CLI sends every request to the dockerd daemon, which delegates to containerd and a runc shim; Podman’s CLI forks a lightweight conmon monitor that supervises crun or runc directly, with no central daemon. Long description: two vertical stacks. On the Docker side, the CLI talks to a root daemon, which talks to containerd, which spawns a runc shim, which starts the container process — a single privileged process sits in the middle of every operation. On the Podman side, the CLI directly spawns a conmon monitor per container, which invokes crun or runc, which starts the container process — there is no shared long-running broker, so each container is an independent process tree.

The Docker client-daemon model

When you run a container under Docker, the docker binary is a thin client. It serializes your request and sends it over a Unix socket (/var/run/docker.sock) to dockerd, which has been running since boot. The daemon consults containerd to manage image storage and container lifecycle, containerd spawns a containerd-shim, and the shim invokes runc to actually create the container using kernel namespaces and cgroups. Once the container is running, the shim keeps it parented so that a daemon restart does not kill your workloads.

This centralization is genuinely convenient. There is exactly one place to query for the state of the world, one API to automate against, and one process to configure for logging, storage drivers, and registry mirrors. It is also why Docker’s developer experience has felt so polished for a decade. The cost is concentrated risk. dockerd historically runs as root, and the Docker socket is effectively a root-equivalent credential: anyone who can write to docker.sock can start a privileged container that mounts the host filesystem and, from there, own the machine. That is not a bug; it is the direct consequence of a root daemon that will do whatever its API is told. Docker’s rootless mode exists to address this and works reasonably well, but it is an opt-in reconfiguration layered on top of a design that assumed root, not the default posture.

There is also a subtler operational cost to a central daemon: it is a shared fate boundary. When dockerd needs an upgrade, a restart, or crashes under a resource storm, it affects every container it supervises at once. Shim parenting mitigates the worst of this — running containers survive a clean daemon restart — but the daemon remains a coordination point that has to be reasoned about during maintenance windows, and its logs, its storage driver, and its API version are a single global configuration that every workload on the host inherits whether it wants to or not. For a single-purpose build box this is fine. On a shared host running many independent workloads, that global coupling is exactly what teams start wanting to escape.

Podman’s daemonless fork-exec and rootless user namespaces

Podman inverts the model. There is no persistent supervising daemon. When you run podman run, the CLI itself sets up the namespaces, then forks and execs the container, handing off supervision to a tiny per-container monitor called conmon. conmon holds the container’s terminal, writes logs, and reports exit status, but it is not a control plane — it manages exactly one container and nothing else. Kill the Podman CLI and your running containers keep going, each parented to its own conmon. There is no single process whose compromise equals total host compromise, because there is no single process in charge.

This architecture is what makes rootless a first-class citizen rather than a bolt-on. In rootless mode, an unprivileged user launches containers inside a user namespace, where the container’s UID 0 (root) is mapped to the user’s own unprivileged UID on the host, and a range of additional container UIDs is mapped to subordinate IDs allocated in /etc/subuid and /etc/subgid. The practical security payoff is large: a process that breaks out of a rootless container lands on the host as an unprivileged user — often mapped to nobody-like permissions — not as root. It cannot read other users’ files, cannot rewrite system binaries, and cannot escalate simply by virtue of having been “root” inside the container.

Rootless Podman maps container root to an unprivileged host user through a user namespace

Figure 2: A host user with UID 1000 creates a user namespace; container UID 0 maps back to host UID 1000 while additional container UIDs draw from the subuid range starting at 100000, so a container escape lands as an unprivileged host user rather than root. Long description: a left-to-right flow showing host user 1000 creating a user namespace, which establishes two mappings — container UID 0 to host 1000, and container UID 1 and above to the subordinate UID range from /etc/subuid — with the endpoint noting that any escape is confined to unprivileged host permissions.

Docker can reach a similar posture with rootless mode, which also uses user namespaces and a slirp4netns-style userspace network. The difference is defaults and friction. Podman is rootless first: run it as a normal user and you get an unprivileged container without touching a config file. Docker rootless requires installing an extra package, running a per-user daemon, and accepting some feature caveats. When your threat model treats “root daemon on a shared host” as unacceptable, the tool that makes rootless the path of least resistance wins the argument on ergonomics, not just on security theory.

Pods and Quadlet: systemd-native supervision

Podman borrows one more idea straight from Kubernetes: the pod. A pod is a group of containers that share a network namespace and can share other resources, exactly like a Kubernetes pod. Podman can even generate Kubernetes YAML from a running pod (podman kube generate) and replay it (podman kube play), which makes local-to-cluster workflows unusually smooth — you can prototype a multi-container unit on your laptop and hand the cluster the same manifest.

But the feature that has genuinely changed how operators deploy is Quadlet. Because Podman has no daemon, the obvious supervisor for a long-running container is the one already running the rest of your Linux host: systemd. Quadlet lets you describe a container in a small declarative unit file — a .container, .pod, .network, or .volume file dropped into /etc/containers/systemd/ or ~/.config/containers/systemd/ — and systemd generates a real service unit from it at boot. The result is a container that starts on boot, restarts on failure, streams logs to journald, respects dependency ordering, and can run entirely rootless, all supervised by the init system you already trust. Podman Quadlet systemd integration turns “how do I keep this container alive” from a daemon-and-restart-policy problem into an ordinary systemd unit, which is exactly how the rest of a Linux server is managed. Docker’s answer to the same problem is the daemon’s own restart policies, which work, but re-implement a supervision layer that systemd already provides. For teams that value a single, auditable service manager over a bespoke one, Quadlet is often the deciding feature all by itself.

Day-to-Day Compatibility and Workflow

Most of the podman vs docker anxiety evaporates the first time you actually drive both. Here is the reassuring part for anyone dreading a migration: the Podman CLI is deliberately command-compatible with Docker. podman run, podman build, podman ps, podman pull, and podman push behave the way the Docker equivalents do, and the near-universal alias docker=podman gets a startling amount of existing tooling and muscle memory working immediately. The compatibility is intentional and maintained, not accidental, so the day-to-day feel of driving containers is close enough that most engineers stop noticing which binary they are typing within a week.

Compose is where the nuance lives, and it is worth being precise. There are two distinct ways to run Compose stacks on Podman. The first is podman-compose, a Python reimplementation that reads your compose.yaml and translates it into Podman commands; it is convenient and ships in most distros, but as a separate project it occasionally lags the official Compose spec on edge-case features. The second — and the one most teams should prefer in 2026 — is running the official Docker Compose v2 binary against Podman’s Docker-compatible API socket. Podman exposes a REST socket that speaks the Docker API, so you enable the Podman socket service, point DOCKER_HOST at it, and the genuine docker compose client drives Podman underneath without knowing the difference. Because it is the real Compose engine, spec fidelity is high and the surprises are few.

Docker Compose can target either the Docker daemon socket or the Podman compatible socket, selecting BuildKit or Buildah for builds

Figure 3: The same docker compose up command can be pointed at either the Docker daemon socket or Podman’s Docker-compatible socket; Docker builds through BuildKit while Podman builds through Buildah, and both converge on running OCI containers. Long description: a top-down flow starting at docker compose up, branching on which API socket is targeted — the Docker daemon socket or the Podman-compatible socket — with the Docker path invoking BuildKit for image builds and the Podman path invoking Buildah, and both paths ending at running containers.

Builds tell a similar story of parity with different engines. Docker builds with BuildKit, which has the most mature layer-caching and parallelization of any OCI builder, and can attach SBOM and provenance attestations to images. Podman builds with Buildah, which powers podman build and is also usable standalone for scripted, daemonless image construction — you can assemble an image line by line in a shell script without a Dockerfile at all. Both produce identical OCI images. In practice, BuildKit’s cache tends to edge out Buildah on large, frequently rebuilt images, while Buildah’s daemonless, rootless build path is a better fit for locked-down CI runners that refuse to grant a Docker socket. If you are standardizing infrastructure tooling around reproducibility, the same daemonless philosophy shows up in our OpenTofu production migration tutorial.

Rootless networking is the part that quietly improved the most. Podman’s networking stack is now built on netavark (the network backend) and aardvark-dns (container name resolution), which replaced the older CNI plugins. For rootless setups specifically, Podman uses pasta (and previously slirp4netns) to provide user-mode networking without root, giving containers outbound connectivity and reasonable performance without a privileged network daemon. The upshot is that rootless container-to-container DNS and multi-network setups now behave much closer to what Docker users expect, closing a gap that used to be a real reason to stay on Docker.

CI is where the daemonless model pays off most concretely. Running Docker inside a CI job traditionally means either mounting the host’s Docker socket into the job — a root-equivalent grant that security teams increasingly forbid — or running Docker-in-Docker with a privileged sidecar. Podman runs rootless in an unprivileged CI container with no daemon and no socket to mount, which is exactly what a hardened pipeline wants. Many GitLab and OpenShift pipelines have already standardized on Buildah and Podman for precisely this reason. The distinction is not academic: a mounted Docker socket in CI is a well-documented privilege-escalation path, because a job that can talk to the daemon can start a privileged container and pivot to the runner host. Removing the daemon removes that pivot entirely, which is why security-conscious teams reach for podman rootless containers in exactly the environments where a compromise would be most expensive.

It is worth naming the ergonomic trade you make in return. The daemonless model means there is no always-warm process holding image layers and metadata in memory, so the very first command in a fresh session does marginally more setup work than talking to an already-running daemon would. In practice this is imperceptible for interactive use and irrelevant in CI, where every job starts cold anyway. The podman vs docker performance conversation, in other words, is mostly a wash at the runtime level; the meaningful differences are in security posture, supervision model, and licensing, not in how fast a container starts. The decision below summarizes where each tool lands across the dimensions that actually drive the choice.

Decision matrix: Podman vs Docker at a glance

Dimension Podman Docker
Architecture Daemonless, fork-exec per container Client-daemon, persistent dockerd
Root requirement Rootless by default Root daemon by default; rootless is opt-in
Central daemon None dockerd supervises everything
Pods Native Kubernetes-style pods No native pod concept
Compose podman-compose or Docker Compose v2 over the Podman socket Docker Compose v2 (native)
Build tool Buildah / podman build BuildKit / docker build
Systemd integration First-class via Quadlet Daemon restart policies
Desktop / licensing Podman Desktop, open source, no seat licensing Docker Desktop paid above ~250 employees / $10M revenue
Kubernetes YAML generation podman kube generate / play Not built in
Ecosystem maturity Strong and growing; some socket-dependent tools need config Deepest ecosystem and tooling defaults

Trade-offs, Gotchas, and What Goes Wrong

No migration is free, and the failure modes in any podman vs docker cutover are predictable enough to plan around. Walk the decision tree, then budget for the sharp edges.

Decision tree for choosing Podman or Docker based on rootless needs, socket tooling, and systemd supervision

Figure 4: Start from whether rootless and daemonless operation is required; if yes, choose Podman, otherwise check for heavy Docker-socket tooling that argues for staying on Docker, then whether systemd-native services push you back to Podman, with a fallback of either tool by team skill. Long description: a branching flowchart beginning at “need containers,” first asking whether rootless and daemonless are required — yes leads to Podman — then whether heavy Docker-socket tooling exists — yes leads to staying on Docker — then whether systemd-native services are wanted — yes leads to Podman — and a final node saying either works, chosen by team familiarity.

The most common surprise is privileged ports. A rootless container cannot bind host ports below 1024 by default, because binding low ports is a privileged operation on Linux. Publishing a rootless service on port 80 fails until you either lower the net.ipv4.ip_unprivileged_port_start sysctl, put a reverse proxy in front, or run that specific service rootful. It is a five-minute fix, but it ambushes people on day one.

Storage and overlay quirks are the second tax. Rootless Podman uses overlay storage in the user’s home directory (~/.local/share/containers), and on some kernels or filesystems it falls back to the slower fuse-overlayfs. Volume permissions also shift because of UID mapping — a bind mount that “just worked” rootful may show files owned by an unexpected UID inside a rootless container, requiring :U mount options or podman unshare to fix ownership. Compose edge cases still exist too: unusual network aliases, certain depends_on conditions, and some Compose extensions can behave differently under podman-compose, which is the strongest argument for using real Docker Compose over the Podman socket instead.

The thorniest category is tooling that assumes a Docker socket. Testcontainers, some IDE integrations, and various CI plugins connect to /var/run/docker.sock and expect the Docker API. Most of them work against Podman once you enable the Podman socket and export DOCKER_HOST, and Testcontainers in particular supports Podman explicitly now — but “works after configuration” is not “works out of the box,” and that configuration step is where migration schedules slip. Audit your Docker-socket-dependent tooling before you commit, not after.

One more migration reality deserves naming: organizational muscle memory. Runbooks say “docker,” onboarding docs say “docker,” Makefiles hard-code docker build, and the alias trick papers over most but not all of it. The technical migration to daemonless containers is usually the easy part; retraining habits and grepping every repository for hard-coded docker invocations is the slow part. Teams that treat the switch as a documentation-and-tooling project, not just a binary install, are the ones that land it cleanly.

Practical Recommendations

The honest way to settle the podman vs docker decision is to stop asking which tool is “better” and start asking which trade-offs your environment can absorb. Podman is the strongest docker alternative available today precisely because it changes the defaults you care about — no root daemon, rootless first, systemd-native — while keeping the image format and most of the CLI identical.

Pick Podman when your threat model rejects a root daemon, when you run untrusted or multi-tenant workloads on shared hosts, when you want containers supervised by systemd via Quadlet, when your CI must build images without a privileged socket, or when Docker Desktop licensing has become a line item you would rather delete. Podman is also the natural choice on Red Hat, Fedora, and increasingly Ubuntu hosts where it is packaged and supported first-class, and for teams already fluent in Kubernetes who value native pods and kube play.

Stay on Docker when your workflow leans hard on Docker-socket tooling you cannot easily reconfigure, when you depend on BuildKit’s specific caching behavior for large builds, when your team’s collective muscle memory and internal tooling are deeply Docker-shaped and the switching cost outweighs the benefit, or when Docker Desktop’s integrated experience is genuinely worth the subscription for your org size. Docker’s ecosystem depth remains its strongest card, and “it works and the team knows it” is a legitimate engineering reason.

A pragmatic migration checklist:

  • Inventory every tool that touches docker.sock and confirm Podman-socket compatibility first.
  • Test your Compose stacks against real Docker Compose v2 over the Podman socket, not just podman-compose.
  • Identify services that bind ports below 1024 and decide proxy-versus-sysctl-versus-rootful per service.
  • Convert long-running services to Quadlet units and validate boot, restart, and journald logging.
  • Verify bind-mount ownership under rootless UID mapping before cutover.
  • Run your CI image builds through Buildah in an unprivileged runner as a proof of concept.

If you are making broader platform bets alongside the runtime, our Kubernetes vs Nomad edge decision matrix applies the same trade-off discipline one layer up.

Frequently Asked Questions

Is Podman a drop-in replacement for Docker?

For most everyday work, close to it, and this is the crux of the podman vs docker question for individual developers. The Podman CLI mirrors Docker’s commands, alias docker=podman covers a large share of scripts, and both consume and produce the same OCI images. The gaps are around tooling that hard-codes the Docker socket, some Compose edge cases, and rootless-specific behavior like low-port binding and volume ownership. Treat it as “compatible after a short configuration pass,” not a literal binary swap, and you will set expectations correctly for a team migration.

Is Podman more secure than Docker?

Architecturally, Podman reduces attack surface by removing the root daemon and defaulting to rootless user namespaces, so a container escape typically lands as an unprivileged host user rather than root. Docker can achieve comparable isolation with rootless mode, but it is opt-in and heavier to operate. Security is never binary — misconfiguration still hurts either way — but Podman’s defaults are the safer starting point, which matters most on shared, multi-tenant, or untrusted-workload hosts.

Does Docker Compose work with Podman?

Yes, in two ways. You can use podman-compose, a Python reimplementation, or — better for spec fidelity — run the official Docker Compose v2 client against Podman’s Docker-compatible API socket by enabling the Podman socket and setting DOCKER_HOST. The second approach uses the genuine Compose engine, so complex stacks behave the way they do on Docker. podman-compose is fine for simple cases but can lag on unusual networking and dependency features.

Do I have to pay for Docker in 2026?

The open-source Docker Engine is free. What carries a subscription is Docker Desktop, which as of 2026 requires a paid plan for organizations above roughly 250 employees or $10M in annual revenue; personal use, education, and smaller businesses remain free. Pricing tiers exist for Pro, Team, and Business plans — treat any specific dollar figures as illustrative and check Docker’s current pricing, since it changes. Podman Desktop is open source with no seat licensing, which is why licensing pressure drives many evaluations.

What is Quadlet and why does it matter?

Quadlet is Podman’s systemd integration. You write a small declarative unit file describing a container, pod, network, or volume, drop it in a systemd directory, and systemd generates a managed service that starts on boot, restarts on failure, and logs to journald — rootless if you want. It matters because it replaces a bespoke daemon-based supervision layer with the same init system that manages the rest of your Linux host, giving you one auditable service manager instead of two.

Can Podman and Docker run on the same machine?

Yes. They store images and containers separately and do not share state by default, so you can install both and use whichever fits a given task while you evaluate. Many teams run this way during migration — driving new work through Podman while legacy Docker-socket tooling keeps functioning — then retire Docker once the tooling audit is complete. Just be aware that podman ps will not show Docker’s containers and vice versa, because there is no shared daemon between them.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *