- Published on
From Docker to Podman — the Complete Guide to Switching to a Daemonless Container Engine
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction — Same Commands, Different Philosophy
- Internals — Daemon vs. fork-exec
- Rootless — a Difference of Defaults
- Config Files — What Lives Where
- GPU — CDI Is the Answer
- Compose — Can You Keep Your compose.yaml?
- Migration Trap Collection — 30 Minutes If You Know, Half a Day If You Don't
- Versions and Communities — Who Builds It, Where It's Discussed
- Closing
- References
Introduction — Same Commands, Different Philosophy
Podman's first impression is "a tool where you swap docker for podman." Indeed, alias docker=podman makes most commands just work, and Docker CLI compatibility is an official goal. But beneath that surface the architecture is nearly the opposite — and every difference you meet during migration (config file locations, GPU setup, compose, permission issues) flows from that architectural difference.
This article organizes those differences from the root. Commands here assume Podman 5.x / Docker Engine 27+.
Internals — Daemon vs. fork-exec
Docker is client-server. When you type docker run, the CLI sends a REST request to the dockerd daemon, which delegates to containerd, which delegates to runc. Every container's ultimate parent is the daemon.
Docker: docker CLI ──REST──▶ dockerd ──▶ containerd ──▶ runc ──▶ container
Podman: podman CLI ──fork/exec──▶ conmon ──▶ crun(runc) ──▶ container
Podman has no daemon. podman run creates the container directly via fork-exec, like an ordinary process. Each container gets a small monitor process, conmon, managing stdio and exit codes, while actual creation is done by the OCI runtime crun (written in C — lighter and faster than runc). Implications:
- No single point of failure. If dockerd dies (or is upgraded), all container management stalls; Podman has no central process to begin with.
- No root daemon socket attack surface.
/var/run/docker.sockis effectively root itself, and attacks targeting it are legion. In Podman that socket simply doesn't exist (a compatibility socket runs only when you want it, with user privileges). - Natural systemd integration. Containers are plain child processes, so systemd units can manage them directly. Modern Podman's recommended path is Quadlet — write a
.containerfile and a systemd service is generated (podman generate systemdis now legacy).
Rootless — a Difference of Defaults
Both engines support rootless mode, but the center of gravity differs. Docker is rootful by default with rootless as opt-in; rootless is Podman's default experience (on Fedora/RHEL you use it as an ordinary user right after install).
The mechanism is user namespaces. Root (UID 0) inside the container maps to your own UID outside; other in-container UIDs map to subordinate ranges assigned in /etc/subuid and /etc/subgid. If a container escapes, all it holds is an unprivileged user's rights.
Rootless constraints to know when migrating:
- No binding ports below 1024 (by default) — lower
sysctl net.ipv4.ip_unprivileged_port_startor use high ports. - Networking goes through a userspace stack (pasta/slirp4netns) — plenty for most workloads; consider rootful for extreme networking needs.
- Storage lives elsewhere — see the config section. This is where
docker system dfinstincts get confused. - Slight startup overhead exists but is irrelevant for long-running services.
Config Files — What Lives Where
Docker's configuration is daemon-centric (daemon.json); Podman's is library-centric and distributed. System-wide config lives in /etc/containers/, per-user config in ~/.config/containers/ (which takes precedence).
Docker Podman
────────────────────────────── ─────────────────────────────────────────
/etc/docker/daemon.json /etc/containers/containers.conf (engine/runtime options)
~/.config/containers/containers.conf
(registry mirrors also daemon.json) /etc/containers/registries.conf (registries, mirrors, blocks)
/etc/containers/storage.conf (storage driver & paths)
/etc/containers/policy.json (image signature policy)
~/.docker/config.json (auth) ~/.config/containers/auth.json (registry auth)
Image/container storage
/var/lib/docker rootful: /var/lib/containers/storage
rootless: ~/.local/share/containers/storage
The two most-touched items during migration: setting unqualified-search registries in registries.conf (Docker resolves nginx to docker.io automatically; Podman by default asks or follows config — add unqualified-search-registries = ["docker.io"] to match Docker), and moving internal mirrors/auth into registries.conf + auth.json.
GPU — CDI Is the Answer
The standard path to NVIDIA GPUs in Podman is CDI (Container Device Interface) — a vendor-neutral spec describing "which device nodes, libraries, and env vars are needed to put this device into a container." It is more transparent and portable than Docker's --gpus runtime-hook approach (Kubernetes' GPU Operator uses CDI under the hood too).
# 1) Install nvidia-container-toolkit, then generate the CDI spec
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
# 2) Check the generated device names
nvidia-ctk cdi list
# nvidia.com/gpu=0, nvidia.com/gpu=all, (with MIG) nvidia.com/gpu=0:0 ...
# 3) Use the GPU in a container
podman run --rm --device nvidia.com/gpu=all \
nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
- Docker's
docker run --gpus allcorresponds to--device nvidia.com/gpu=all(recent Podman accepts--gpusas a compat flag, but CDI notation is canonical). - GPUs work rootless. The GPU driver operates in kernel space, so container privilege level doesn't affect performance. If the system-wide
/etc/cdi/nvidia.yamlis readable, it's used as-is; otherwise generate the spec in user space and point Podman at that directory. - If the GPU disappears after a driver update, you almost certainly forgot to regenerate the CDI spec (
nvidia-ctk cdi generate).
Compose — Can You Keep Your compose.yaml?
Bottom line: mostly yes, and there are two ways.
Option 1 — point the Docker Compose binary at the Podman socket (recommended). Podman can serve a Docker-API-compatible socket. With it enabled, standard docker compose works unchanged with Podman as the backend — no compose.yaml rewrite.
# Enable the user socket (rootless)
systemctl --user enable --now podman.socket
# Point docker compose at the Podman socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
docker compose up -d # backend is Podman
You're running the official Go implementation of the Compose spec, so compatibility is maximal. The podman compose command itself is a wrapper that calls an external compose provider (docker-compose if installed).
Option 2 — podman-compose. A separate project reimplemented in Python. Its virtue is purity — it invokes podman commands directly with no daemon/socket — but corners of the Compose spec (some network options, profiles, advanced depends_on conditions) can differ subtly from the reference implementation. Fine for simple stacks; for complex compose files, Option 1 is safer.
Compose-related migration notes:
restart: alwaysdoes not mean "start on boot" in daemonless Podman — the Podman-native answer for boot autostart is Quadlet/systemd.- For production leanings, also consider
podman kube play(run Kubernetes YAML directly) instead of compose — unifying local and Kubernetes manifests.
Migration Trap Collection — 30 Minutes If You Know, Half a Day If You Don't
- SELinux volume labels: on Fedora/RHEL, if a volume mount hits Permission denied, it's almost certainly SELinux. Add
:Z(private) or:z(shared) —-v ./data:/data:Z. The single most common trap for compose files arriving from Ubuntu. - Unqualified image names: without
unqualified-search-registriesset,podman pull nginxbehaves differently from Docker. In CI scripts, the most robust fix is fully qualified paths likedocker.io/library/nginx. - Rootless storage space: images pile up in your home directory (
~/.local/share/containers). On servers with small homes, movegraphrootinstorage.conf. - Tools that assume the Docker socket (Testcontainers, some CI): mostly solved by Option 1's Podman socket +
DOCKER_HOST. - The
podman-dockerpackage: a shim that routes thedockercommand to podman. Useful for script compatibility — but do tell your teammates that "docker on this server is actually podman." - Gradual migration works: the two engines have separate storage and can coexist on one machine. Move service by service.
Versions and Communities — Who Builds It, Where It's Discussed
Version sense (as of 2026): Podman is in its 5.x generation — the 4.x → 5.0 transition switched default networking to pasta and matured CDI support. It is deeply integrated into RHEL 9/10, and Fedora has shipped Podman as the default container tool for several releases. Docker Engine is in the 27+ generation and still owns the largest ecosystem and documentation base.
Governance and community:
- Docker — a dual structure of Docker, Inc.'s commercial products plus open source (moby/moby). Docker Desktop requires a paid subscription beyond a certain company size (the engine itself is open source). Its greatest asset is the vast accumulation of tutorials and Stack Overflow answers.
- Podman — part of the Red Hat-led containers GitHub organization, family to Buildah (image building) and Skopeo (image inspection/copying). All Apache-2.0 open source, and Podman Desktop is free. Discussion happens on GitHub (containers/podman), the Podman mailing list, and Matrix channels; adoption is especially strong in RHEL-family enterprise environments.
A practical picking rule: if your team runs RHEL/Fedora, requires rootless security, or prefers systemd-based operations, Podman is the natural fit. If you depend on Docker Desktop tooling or need the ecosystem's documentation mass, Docker remains the safe default. And both stand on the same OCI standard — an image built on either runs on either. Such is the blessing of having standards.
Closing
Moving from Docker to Podman is not a command swap but an operating-model swap — daemon to fork-exec, rootful to rootless, daemon.json to the containers.conf family, --gpus to CDI, compose-daemon dependence to socket compatibility or systemd/Quadlet. Thankfully, the OCI standard carries your artifacts — images and compose.yaml — across nearly untouched, and the traps converge to the few above (SELinux labels, unqualified images, storage paths). To drill container fundamentals hands-on, try this site's container lab and Linux terminal.
References
- Podman documentation · containers/podman (GitHub)
- NVIDIA Container Toolkit — CDI support
- Container Device Interface spec
- Podman — Quadlet documentation (systemd integration)
- podman-compose (GitHub) · Docker Compose
- Run NVIDIA GPU Containers with Podman (OneUptime)
- Podman vs Docker 2026 benchmarks (lucaberton.com)