Podman vs Docker: Rootless Containers, Pods, and the Case for Switching
Docker democratized containers. It gave developers a simple, consistent way to package and run software that has fundamentally changed how we build and deploy applications. If you’ve been running containers for any length of time, Docker is almost certainly where you started.
But Docker carries architectural baggage that matters more the longer you operate in production: a persistent root daemon, a socket that is effectively a root shell, rootless mode bolted on as an afterthought, and no native concept of container grouping. These aren’t dealbreakers for many use cases, but they’re real constraints — particularly if you care about security, systemd integration, or preparing workloads for Kubernetes.
Podman takes a different approach. Developed by Red Hat and now a core part of RHEL, CentOS Stream, and Fedora, it reimagines the container runtime without a daemon, makes rootless containers a first-class default, introduces Kubernetes-compatible pods directly on the command line, and integrates with systemd in ways Docker simply cannot. And it does all of this while remaining compatible enough with Docker that alias docker=podman genuinely works for most workflows.
This guide is for people who know Docker well. We’ll cover the architectural differences in depth, work through rootless containers and pods with real examples, walk through Buildah and Skopeo, and give you a concrete migration path.
Part 1: What is Podman?
Podman (Pod Manager) is an OCI-compliant container engine developed by Red Hat. It is not a standalone tool — it belongs to a family of tools designed to cover the full container lifecycle without requiring a privileged daemon:
- Podman — run, manage, and inspect containers and pods
- Buildah — build OCI-compatible container images
- Skopeo — inspect, copy, and sync images across registries
All three tools speak the same OCI image format as Docker, can pull from and push to the same registries (Docker Hub, GHCR, Quay.io, private registries), and produce images that run on any OCI-compliant runtime including Docker itself.
Installing Podman
On RHEL 8/9, CentOS Stream, and Fedora, Podman is installed by default or available directly:
|
|
The Drop-In Replacement Claim
Podman is designed to be a drop-in CLI replacement for Docker. Most docker commands run unchanged:
|
|
For a truly seamless transition:
|
|
This alias works for the majority of Docker workflows. The edge cases where it breaks are well-documented, and we’ll cover the important ones in the migration section.
Part 2: The Core Difference — Daemon vs. Daemonless
Docker’s Architecture
When you install Docker, you’re installing a layered system of processes:
dockerd (Docker daemon — runs as root)
└── containerd (container lifecycle management)
└── containerd-shim
└── runc (OCI runtime — actually runs containers)
The Docker daemon (dockerd) is a persistent root process. Every Docker operation — pulling images, starting containers, reading logs — goes through it via the Docker socket at /var/run/docker.sock.
That socket is the problem. Because the daemon owns it and runs as root, any process that can write to /var/run/docker.sock can instruct Docker to do anything root can do. The canonical example:
|
|
That’s a complete host filesystem escape. This is why adding a user to the docker group is functionally equivalent to granting them passwordless sudo. It’s widely understood, frequently ignored, and a real risk in multi-user environments.
Additionally, the daemon is a single point of failure. If dockerd crashes or gets OOM-killed, all containers lose their lifecycle management — they may continue running but you lose the ability to stop, inspect, or restart them through Docker until the daemon recovers.
Podman’s Fork-Exec Model
Podman has no daemon. When you run a container with Podman, the Podman binary forks and execs the container directly:
podman run nginx
└── conmon (container monitor — tiny, per-container)
└── runc (OCI runtime)
└── nginx (your process)
conmon (container monitor) is a small C program that serves as the container’s parent process — it holds open the container’s stdio streams and allows Podman to reconnect to a running container after the initial podman run process exits. It’s per-container, not a shared daemon.
The practical consequences:
- No persistent root process required. Non-root users run containers as their own processes.
- No socket required for basic operations. Podman talks directly to the kernel via the OCI runtime.
- Smaller attack surface. There’s nothing listening that didn’t exist before you ran
podman. - No single point of failure. A Podman crash doesn’t affect running containers. They’re just processes.
- Process visibility. Running containers appear in
ps auxas child processes of the user who started them.
|
|
The trade-off: because there’s no daemon coordinating state across sessions, some features require more setup (like the Podman socket for applications that need the Docker API).
Part 3: Rootless Containers
What Rootless Means
A rootless container is one where both the container manager (Podman) and the containers themselves run as an unprivileged user with no elevated host privileges. “Root inside the container” doesn’t mean root on the host — the container’s UID 0 maps to your unprivileged user ID on the host via Linux user namespaces.
Docker’s Rootless Mode vs. Podman Rootless
Docker does have a rootless mode, but it’s an add-on with friction:
|
|
Docker’s rootless mode is not the default, is not always available on managed systems, and has a separate socket path that breaks tools which hardcode /var/run/docker.sock. It’s a valid option but clearly an afterthought.
Podman’s rootless mode is the default. Run podman run as any user and it works — no setup, no export, no separate mode to enable:
|
|
User Namespaces and UID Mapping
The mechanism enabling this is Linux user namespaces. When Podman starts a rootless container, it maps a range of subordinate UIDs and GIDs from the host into the container’s namespace.
Two files control the available ranges:
|
|
This means user jmoon has UIDs 100000–165535 available for use inside containers. When a process inside the container runs as UID 0 (root), the kernel maps that to UID 100000 on the host. When it runs as UID 1000, it maps to UID 101000. From the host’s perspective, these are all unprivileged UIDs with no special privileges.
If your user isn’t in /etc/subuid and /etc/subgid, add entries:
|
|
You can inspect the actual mapping for a running container:
|
|
Rootless Storage
Rootless Podman stores everything in your home directory, not in /var/lib/docker:
~/.local/share/containers/storage/ # images and container layers
~/.config/containers/ # configuration files
$XDG_RUNTIME_DIR/containers/ # runtime state (sockets, etc.)
This is great for multi-user systems — each user has completely isolated container storage with no interference between users.
Limitations of Rootless Containers
Rootless is not magic. There are real constraints:
Port binding below 1024. Unprivileged users can’t bind to ports under 1024 by default:
|
|
No macvlan/ipvlan networking. These network modes require root because they interact with network interfaces directly. Rootless networking uses slirp4netns or pasta (user-space networking), which adds a small overhead.
No cgroup v1 resource limits. Rootless containers require cgroup v2 (unified hierarchy) for CPU and memory limits. Most modern Linux systems (kernel 5.10+, systemd 248+) default to cgroup v2, so this is rarely an issue today.
Bind-mounting root-owned host directories. If you need to mount /var/log or similar root-owned paths, you’ll need sudo or need to use a rootful Podman container.
Enabling User Services with Lingering
By default, a user’s systemd session ends when they log out, killing all their processes — including containers. To run rootless Podman containers that survive logout and start at boot:
|
|
Security Implications
The key security property: a container escape in a rootless container does not give the attacker root on the host.
In a rootful Docker setup, if a container escape exploit is triggered, the attacker gets a UID 0 shell on the host. With rootless Podman, they get a shell running as UID 100000 (or wherever the UID mapping starts) — an unprivileged user with no special access.
This isn’t a complete security story on its own — proper seccomp profiles, SELinux/AppArmor labels, and limited capabilities still matter — but it meaningfully reduces the impact of a container escape.
Part 4: CLI Compatibility and Differences
Commands That Work Identically
The vast majority of daily Docker commands work without modification:
|
|
Docker Compose → podman compose
Podman 4.0 introduced a built-in podman compose subcommand (via podman-compose under the hood). Podman 4.4+ includes it as a proper built-in. For older Podman versions, install podman-compose via pip:
|
|
Usage is nearly identical to Docker Compose:
|
|
Compatibility is good but not perfect. Very complex Compose files using advanced networking or Docker-specific extensions may need adjustment. For most homelab stacks, it works as expected.
The Docker Socket and Podman’s API
Some applications (Traefik, Watchtower, Portainer, Diun) use the Docker socket API to discover and manage containers. Podman provides a compatible socket:
|
|
With DOCKER_HOST set, tools like Docker Compose and many CI utilities will use the Podman socket transparently.
Behavior Differences Worth Knowing
Default registries. Docker defaults to Docker Hub; Podman searches a list of registries configured in /etc/containers/registries.conf. If you get “short-name resolution” prompts, that’s why:
|
|
To disable the prompt (just use Docker Hub by default):
|
|
--latest flag. podman ps --latest shows the most recently created container. Docker doesn’t have this flag.
podman play kube vs no Docker equivalent. This is unique to Podman.
DOCKER_HOST environment variable. Podman respects this variable, so setting it to the Podman socket makes everything work seamlessly.
Part 5: Pods — The Kubernetes-Inspired Feature
Pods are the feature that most clearly shows where Podman is heading and why it matters for anyone working with or toward Kubernetes.
What a Pod Is
A pod is a group of one or more containers that share:
- Network namespace — all containers in a pod share the same IP address and can communicate via localhost
- IPC namespace — shared inter-process communication
- Optionally PID namespace — shared process table
This is exactly how a Kubernetes pod works. It’s the same concept, running directly on your workstation or server without a Kubernetes cluster.
The pod model is powerful because it enables the sidecar pattern: run a main application container alongside helper containers (logging agents, proxies, init containers, secret-fetchers) that share the same network context.
Pod Lifecycle Commands
|
|
The Infra Container
Every Podman pod automatically creates an infra container (also called a pause container). This small container (based on k8s.gcr.io/pause or the Podman equivalent) holds the network and IPC namespaces open for the lifetime of the pod. Other containers join the pod’s namespaces by attaching to the infra container.
|
|
This design means the pod’s IP address remains stable even if individual containers are stopped and restarted — a key property that mirrors Kubernetes behavior.
Practical Example: Web App + Sidecar Logger
Let’s build a realistic pod: an nginx web server with a Fluent Bit sidecar that reads nginx’s access log and ships it to a logging backend.
|
|
A minimal fluent-bit.conf for this setup:
|
|
Generating Kubernetes YAML from a Pod
This is the migration path from Podman to Kubernetes. Once you’ve built and validated a pod locally, Podman can generate the Kubernetes manifest:
|
|
The output is a valid Kubernetes Pod manifest:
|
|
Apply it to a Kubernetes cluster (or back to Podman):
|
|
podman kube play also supports Deployments and ConfigMaps, making it a useful local Kubernetes emulator.
Part 6: Systemd Integration
Docker’s relationship with systemd is awkward. The recommended way to manage Docker containers as services involves writing systemd unit files that run docker start commands, dealing with race conditions, and bolting on restart logic that Docker itself also handles. It works, but it’s inelegant.
Podman was designed with systemd in mind from the start.
Legacy Method: podman generate systemd
The original approach generates a unit file from a running container:
|
|
This method works but is deprecated in favor of Quadlets in Podman 4.4+.
Quadlets: The Modern Approach
Quadlets (introduced in Podman 4.4, integrated from the quadlet project) define containers as systemd unit files using a declarative INI-style format. They’re simpler to write, easier to read, and generate proper systemd service units automatically.
Drop files into ~/.config/containers/systemd/ (rootless) or /etc/containers/systemd/ (rootful), then reload systemd.
Supported unit types:
.container— a single container.pod— a pod.network— a container network.volume— a named volume.image— an image pull unit
Complete Quadlet Example: Web Service
Create ~/.config/containers/systemd/myapp.container:
|
|
Enable it:
|
|
Quadlet for a Pod
~/.config/containers/systemd/webapp.pod:
|
|
~/.config/containers/systemd/webapp-app.container:
|
|
Quadlet for Volumes and Networks
~/.config/containers/systemd/myapp-data.volume:
|
|
~/.config/containers/systemd/myapp-net.network:
|
|
Reference them in your .container file:
|
|
The .volume and .network suffixes tell Quadlet to depend on those units automatically.
Auto-Update with podman auto-update
Combined with the io.containers.autoupdate=registry label, Podman can automatically check for new image versions and restart containers with the updated image:
|
|
The podman-auto-update timer runs daily by default and is a clean alternative to Watchtower for Podman-based setups.
Part 7: Buildah — Building Images Without Docker
Buildah is the image builder in the Podman family. It builds OCI-compliant images and can be used as a direct replacement for docker build.
Dockerfile-Based Builds
|
|
buildah bud (build using Dockerfile) accepts the same flags as docker build and works with existing Dockerfiles.
Buildah’s Scripting Approach
Buildah’s unique capability is building images through a shell script rather than a Dockerfile. This gives you the full power of bash for conditional logic, dynamic configuration, and complex build workflows:
|
|
This scripting approach is particularly useful for conditional builds:
|
|
Rootless Image Builds
Building images rootlessly works out of the box with both Buildah and podman build:
|
|
Why Buildah Matters in CI
In CI environments, the traditional approach requires either a Docker daemon or Docker-in-Docker (DinD), both of which have security and complexity implications. Buildah builds images rootlessly without a daemon:
|
|
This is cleaner than DinD and works in environments where privileged containers are not allowed.
Part 8: Skopeo — Working With Registries
Skopeo is the specialist tool for registry operations. It works with remote images without ever pulling them locally into container storage.
Inspect Remote Images Without Pulling
|
|
Copy Images Between Registries
|
|
Sync Entire Registries or Repositories
|
|
Delete Images From Registries
|
|
Practical CI Use Cases
Skopeo is particularly valuable in CI/CD pipelines for:
- Air-gapped registry pre-seeding — sync required images to an internal registry before builds start
- Registry migration — copy all images from one registry to another without intermediate storage
- Image promotion — copy an image from
dev-registrytoprod-registrywhen a pipeline stage passes (no re-build) - Manifest inspection — verify image digests and metadata in pipeline gates without pulling
|
|
Part 9: Migrating From Docker to Podman
A production-grade migration typically goes through five stages. Most homelabs can move faster, but this structure works at any scale.
Step 1: Install Podman and Set the Alias
|
|
Step 2: Migrate Volumes
Docker stores named volumes in /var/lib/docker/volumes/. Podman rootless stores them in ~/.local/share/containers/storage/volumes/.
|
|
For bind mounts, no migration is needed — just point to the same path.
Step 3: Migrate Docker Compose Workflows
|
|
If podman compose has compatibility issues, try the pip version which may have better coverage:
|
|
Step 4: Convert to Quadlets (Optional but Recommended)
For production services, migrate from Compose to Quadlets for proper systemd integration:
|
|
Step 5: Update CI/CD Pipelines
|
|
Common Migration Gotchas
Traefik’s Docker provider uses /var/run/docker.sock. Point it to the Podman socket instead:
|
|
Watchtower — Watchtower needs the Docker socket. The same socket substitution works:
|
|
However, for Podman-native auto-updates, podman auto-update with the io.containers.autoupdate label is a cleaner solution.
Portainer supports Podman via the socket:
|
|
Rootless networking with ports below 1024. Applications that need port 80 or 443 require either the sysctl fix or a rootful Podman setup:
|
|
Network differences. Rootless Podman uses slirp4netns or pasta for user-space networking rather than a kernel bridge. This means containers are NAT’d through the host, just like rootful Docker, but there’s no native bridge network. Most workloads are unaffected. If you need containers to have their own IPs on the host network, you’ll need rootful Podman.
Image short names. Where Docker defaults to Docker Hub, Podman may prompt you to choose a registry. Fix it in config:
|
|
Part 10: Podman Desktop
Podman Desktop is a cross-platform GUI application analogous to Docker Desktop. It’s open source and free without licensing restrictions.
Available on: Linux (Flatpak, RPM, DEB), macOS, Windows
Key features:
- Container and pod management with a visual interface
- Image management and builds
- Kubernetes integration — view and manage local clusters (Kind, Minikube, k3s)
- Extensions for Compose, OpenShift, and more
- Dashboard with container health, resource usage, and logs
- Built-in terminal for container exec
- Import and export Docker Compose stacks
|
|
On macOS and Windows, Podman Desktop manages a Linux VM (similar to Docker Desktop’s VM model) to run containers, since the container runtime requires a Linux kernel. On Linux, it talks directly to the Podman socket.
For homelabbers who prefer a GUI, Podman Desktop is a compelling alternative to Docker Desktop — especially since Docker Desktop requires a paid subscription for organizations over 250 employees or $10M in revenue.
Part 11: When to Stick with Docker
Podman is not the right choice for every situation. Honest trade-offs:
Docker Desktop on macOS/Windows is more polished. The developer experience — especially volume performance on macOS via VirtioFS, and the integrated Docker Extensions marketplace — is more mature than Podman Desktop. If most of your team works on Macs and Docker Desktop’s licensing isn’t a concern, the familiarity and polish may outweigh Podman’s security advantages.
Docker Compose V2 is more mature. The docker compose V2 plugin is more feature-complete and better tested than podman compose. Complex Compose files with depends_on: condition: service_healthy, custom networking, and Docker-specific extensions are more reliably handled by Docker Compose.
Larger ecosystem integration. Tools like Portainer, Yacht, and many CI systems have deeper, better-tested Docker socket integration. Podman’s socket compatibility is good, but edge cases exist. If you rely heavily on the ecosystem, Docker reduces friction.
Team familiarity and documentation. Docker has more tutorials, Stack Overflow answers, and vendor documentation than Podman. For teams new to containers, the learning resources for Docker are more abundant.
Container orchestrators beyond Kubernetes. If you’re using Nomad, Docker Swarm, or other orchestrators that use the Docker daemon API, Podman isn’t a fit.
Part 12: Side-by-Side Comparison
| Feature | Docker | Podman |
|---|---|---|
| Architecture | Daemon (dockerd) running as root |
Daemonless, fork-exec model |
| Rootless containers | Opt-in, requires manual setup | Default, first-class feature |
| Pod support | No native concept | Yes, Kubernetes-compatible pods |
| Systemd integration | Awkward, workarounds needed | Native via Quadlets, auto-update |
| Compose support | Docker Compose V2 (mature) | podman compose / podman-compose (good, less mature) |
| Windows/macOS | Docker Desktop (polished, licensing restrictions) | Podman Desktop (open source, less polished) |
| Kubernetes YAML | No native generation | podman generate kube / podman kube play |
| Security model | Root daemon, socket = root access | No persistent root process, smaller attack surface |
| Container escape impact | Attacker gets host root | Attacker gets unprivileged UID (100000+) |
| RHEL/enterprise support | Third-party, requires subscription | First-party Red Hat, in RHEL 8+ by default |
| Image format | OCI-compatible | OCI-compatible |
| Registry tools | Limited (docker save/load) | Full suite: Podman + Buildah + Skopeo |
| Building images | docker build (requires daemon) |
podman build / buildah bud (rootless, daemonless) |
| CI/CD builds | Docker-in-Docker or bind-mount socket | Rootless build, no daemon required |
| Auto-updates | Watchtower (third-party) | podman auto-update (built-in) |
| License | Apache 2.0 (engine), Docker Desktop paid for orgs | Apache 2.0, all components free |
Putting It All Together
Podman’s strengths align well with a specific set of concerns: security-conscious production environments, RHEL/Fedora-based infrastructure, Kubernetes preparedness, and systemd-native service management. If those concerns match your environment, Podman is not just an alternative to Docker — it’s a genuinely better fit.
The migration path is low-risk. Start with the alias, migrate your most important service, and run both in parallel. For homelab use, the combination of rootless containers + Quadlets + podman auto-update is a particularly clean stack: containers run as your own user, start at boot via systemd, and stay up to date automatically — no daemon, no root, no Watchtower.
The Buildah + Skopeo pairing rounds out the story for anyone building or distributing images. Rootless image builds without DinD are meaningful in CI environments with strict security policies, and Skopeo’s registry-to-registry copy capability is something Docker simply doesn’t have a clean answer for.
If you’re on Fedora or RHEL, Podman is already there. If you’re on Debian or Ubuntu, it’s an apt install away. The alias works. Start there, and you’ll find Podman earns its place incrementally.
Further reading: Podman documentation — Buildah documentation — Skopeo on GitHub — Podman Desktop — Quadlet documentation
Comments