Docker is the default. It is what tutorials assume, what CI systems ship with, and what everyone on your team already knows. Choosing something else needs a reason better than novelty.
I run a single VPS that hosts a few personal projects. I manage it alone. After running both on that machine, I moved to Podman and stayed there. This is the reasoning, including the parts where Docker is genuinely the better choice.
The Architectural Difference
Nearly every practical difference follows from one design decision.
Docker uses a client–server model. The docker command is a thin client that talks to dockerd, a long-running background daemon owning every container on the machine. dockerd runs as root.
docker CLI ──► dockerd (root) ──► containerd ──► your containersPodman forks and executes directly. There is no daemon. podman run starts the container as a child process of your shell, supervised by a small conmon process. Containers are children of whoever started them.
podman ──► conmon ──► your containerThat is the whole difference. Everything below is a consequence of it.
What the Daemon Actually Costs
I want to be fair here, because “daemonless” gets repeated as a slogan more often than it gets explained.
It is a single point of failure. If dockerd crashes or is restarted badly, it takes your containers with it. Docker has worked hard on live-restore to soften this, but the coupling is real: one process failure affects everything on the host.
It is root, and it owns everything. Every container on the machine is a child of a root-owned daemon. That is a large privileged surface for something that is running arbitrary images you pulled off the internet.
The docker group is root. This is the part that surprises people. Adding a user to the docker group is documented everywhere as the way to avoid typing sudo. What it actually grants is the ability to talk to a root-owned socket that will happily start a container mounting the host filesystem:
docker run -v /:/host -it alpine chroot /hostThat is a root shell. No password, no sudo, no audit trail that looks like privilege escalation. Membership in the docker group is equivalent to passwordless root, and it does not feel like it when you are adding yourself to it.
Podman has no socket, no group, and nothing to escalate through.
Rootless, Properly Explained
“Rootless” sounds like a small convenience. What it actually means is worth understanding, because it is the strongest argument in Podman’s favour.
Linux user namespaces let a process have a different view of user IDs than the rest of the system. Inside the namespace a process can be UID 0 — full root, as far as it can tell. Outside, the kernel translates that to some unprivileged UID.
The mapping is allocated in advance, in /etc/subuid and /etc/subgid:
admin:100000:65536That reserves 65,536 UIDs starting at 100000 for the admin user. When a container thinks it is root, the kernel sees UID 100000 — an account that owns nothing and can do nothing.
You can watch the translation directly:
podman unshare cat /proc/self/uid_mapThe practical consequence: a container escape lands the attacker in an unprivileged account, not on root. They cannot read /etc/shadow, cannot write to system directories, cannot install anything. They are in a sandbox that the kernel enforces rather than one the container runtime politely maintains.
Docker does support rootless mode now, and it works. The difference is that in Podman this is the default path — the one the documentation assumes, the one every feature is tested against — while in Docker it remains a deliberate opt-in with its own installation procedure and a set of known limitations.
Defaults matter more than capabilities. The secure path being the easy path is most of what makes it get used.
What It Does Not Protect Against
Worth being honest: rootless is not a security boundary against everything. A kernel exploit is still a kernel exploit, and user namespaces have had their own vulnerabilities. Application-level compromise — someone exploiting your web app — is entirely unaffected by any of this.
What it removes is one specific and very common escalation path: container root becoming host root. That is a meaningful reduction, not a force field.
The systemd Argument
This is the one that actually decided it for me, and it gets discussed the least.
My server already runs systemd. systemd already knows how to start things at boot, restart them on failure, order dependencies, apply resource limits, and collect logs. It has done this reliably for a decade.
Docker’s restart policies reimplement a subset of that inside the daemon. So a Docker host runs two supervisors: systemd supervising dockerd, and dockerd supervising containers. Boot ordering between a container and a host service becomes awkward. Logs live in a separate place from every other log on the system. systemctl status cannot tell you whether your application is healthy.
Podman’s Quadlet collapses this. You write a file that looks like a systemd unit, describing a container:
[Container]
ContainerName=web
Image=docker.io/library/caddy:2-alpine
PublishPort=80:80
[Service]
Restart=always
[Install]
WantedBy=default.targetA systemd generator turns that into a real service. The container is a systemd unit. Which means systemctl status web works, journalctl -u web has the logs, dependency ordering uses After= like everything else, and boot startup is handled by the thing that already handles boot startup.
One supervisor instead of two. On a single server I maintain alone, that simplification was worth more than any individual feature. I wrote a full guide to the format in Podman Quadlet: Running Containers as systemd Services.
Compatibility Is Better Than Expected
A real worry before switching: how much would break?
Images are identical. Both use the OCI standard. Every image on Docker Hub works unmodified.
The CLI is a drop-in. podman run, podman build, podman ps, podman logs, podman exec — same commands, same flags. Many people just alias docker=podman and forget about it.
Compose files work, via podman compose delegating to docker-compose or podman-compose.
The Docker API is available if some tool insists on it. Podman can expose a compatible socket:
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix:///run/user/$UID/podman/podman.sockTools that talk to docker.sock — Testcontainers, various CI runners — generally work through this.
One deliberate difference worth knowing: Podman does not assume Docker Hub. Write alpine and it will ask which registry you meant, or fail. You write docker.io/library/alpine in full. Mildly annoying for a week; then you realise that an unqualified image name is genuinely ambiguous and that being explicit is correct.
Where Docker Is Still the Right Choice
I would not move a team to Podman on the strength of the above. The honest cases for staying on Docker:
Your team already knows Docker. Familiarity is a real engineering asset. The security benefits above do not outweigh everyone being slower and hitting unfamiliar errors.
Docker Desktop. On macOS and Windows, Docker Desktop is a smoother experience than Podman Desktop, particularly around filesystem performance and networking. Podman Machine works, but it is less polished.
Tooling that assumes docker.sock. The compatibility socket covers most of it, but “most” is doing work in that sentence. If your CI, your IDE plugins, and your local orchestration all assume Docker, the friction adds up.
Swarm, or anything built on it. Podman has no equivalent. If you use Swarm, this conversation is over.
BuildKit-specific features. Buildah is capable, but some advanced BuildKit cache mounts and secret-mount syntax do not translate cleanly.
Large teams with existing platform tooling. The cost of switching scales with how much you have built around Docker, and that cost is rarely justified by the benefits on a well-managed platform.
What Podman Costs You
Being fair in both directions. Things that were worse:
Rootless networking is slower. Traffic goes through a userspace proxy (pasta, or slirp4netns on older versions) rather than straight through the kernel. For a personal site serving modest traffic this is invisible. Under heavy load it is measurable.
Privileged ports need a decision. A rootless container cannot bind port 80 or 443 without either lowering net.ipv4.ip_unprivileged_port_start, granting a capability, or redirecting. Not hard, but it is an extra thing to know before your web server will start.
Volume permissions get confusing. User namespaces mean the UID inside the container is not the UID outside. A bind-mounted directory owned by your user may appear owned by nobody inside. podman unshare chown and the :U volume flag exist for this, and you will need them at some point.
Documentation assumes Docker. Most tutorials, most Stack Overflow answers, most vendor guides. Usually the translation is mechanical, but you are doing the translating.
Some images assume they are root. Images that chown at startup or write to system paths can misbehave under user namespace mapping. Well-built images are fine; older ones are a coin flip.
How I Decided
The criteria that actually mattered, in order:
- Who runs this? One person, one machine. No team convention to respect, no platform to integrate with. This is where Podman is at its strongest and Docker’s ecosystem advantages count for least.
- What is the worst realistic failure? A compromised container on an internet-facing box. Rootless narrows what that becomes.
- How much do I want to maintain? As little as possible. One supervisor instead of two, no daemon to keep alive, no privileged group to remember I am in.
- Can I explain the whole thing? With Quadlet, the entire deployment is a handful of unit files I can read top to bottom. That property is worth a lot at 2 AM.
For a personal VPS, those all point the same direction. For a company with a platform team and existing Docker infrastructure, several of them point the other way — and that is a legitimate answer, not a compromise.
Pick based on which of those questions your situation actually asks.
If you want to see it in practice, this setup is the subject of a five-part walkthrough starting at Setting Up a VPS, Part 1, with the container work in Part 3.