The Docker daemon traditionally runs as root, which means any container escape — rare, but not theoretical — has a path to root on the host. Podman's rootless mode removes that single point of privilege entirely: containers run as your own unprivileged user, using user namespaces to remap container UIDs without ever touching real root.

Why rootless was worth pursuing

For a fleet running a mix of internal tools and a few client-facing services, none of which had a pressing security incident motivating the change, this was a "before it's a problem" migration rather than a reactive one. The appeal of rootless Podman is structural: even in a worst-case container escape scenario, the attacker lands as an unprivileged user, not root.

Podman also runs daemonless — each container is a direct child process rather than managed by a long-running privileged daemon, which removes an entire class of "the daemon itself got compromised" concerns that Docker's architecture doesn't fully avoid.

Compose compatibility

Podman supports Docker Compose files directly via podman-compose, or through Podman's own Docker-API-compatible socket for tools that expect to talk to a Docker daemon:

Enabling the Docker-compatible socket

systemctl --user enable --now podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

With that socket active, most existing Compose files and even the regular docker CLI work against Podman with no changes — the compatibility layer covers the large majority of day-to-day commands.

What actually broke

Three things needed real changes, none of them showstoppers but all worth knowing in advance:

  • Bind-mounted volume permissions. Because rootless containers map UIDs through user namespaces, a bind mount that worked under Docker's root-owned files sometimes showed up with unexpected ownership inside the container. The fix is usually an explicit UID/GID mapping in the Podman run command or Compose file.
  • Privileged port binding. Binding to ports below 1024 as an unprivileged user isn't allowed by default. We adjusted net.ipv4.ip_unprivileged_port_start via sysctl rather than running anything as root, which keeps the rootless guarantee intact.
  • A handful of images that assumed root. A couple of older images explicitly expected to run as UID 0 inside the container and broke under the UID remapping. These needed minor Dockerfile changes to run correctly as a non-root user — arguably a fix that should have happened regardless of this migration.

Allowing unprivileged binds below 1024

sysctl net.ipv4.ip_unprivileged_port_start=80
!

Lowering this value system-wide has some security tradeoffs of its own — it allows any unprivileged process to bind low ports, not just your Podman containers. Weigh this against your specific threat model rather than applying it reflexively.

Networking differences

Rootless Podman's default networking goes through slirp4netns or, more recently, pasta, both of which add a small amount of overhead compared to Docker's root-mode bridge networking — noticeable on high-throughput services, negligible for most everyday workloads. For the one service in our fleet handling meaningful request volume, pasta's overhead was small enough not to require any compensating change.

The verdict, six months in

Six months and zero security incidents later — though that's a low bar for six months — the migration has been quietly successful. The bind-mount and privileged-port issues were one-time fixes, not ongoing friction. Day-to-day operations, deploys, and monitoring all work the same as before, with one structural improvement sitting underneath that I no longer have to think about.

Would I recommend it broadly? For new projects starting fresh, defaulting to rootless Podman from day one is close to a free security improvement. For an existing large Docker fleet, the migration cost is real but front-loaded — the fixes happen once, and the benefit is ongoing.

ML

Mikko Laaksonen

DevOps engineer based in Tampere, Finland. Writes about Linux, Docker, databases, and web design.

More about Mikko →