| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64podman-docker-5.8.2-5.el10_2.x86_64 (optional docker CLI shim) |
| Applies to | Any Linux host comparing Podman and Docker Engine; Docker Desktop on macOS and Windows noted where workflows diverge |
| Privilege | Normal user or root (lab examples use rootful Podman) |
| Scope | Decision-focused comparison of architecture, commands, images, rootless models, Compose, networking, pods, builds, systemd, and CI tooling. Does not include full migration runbooks, Compose tutorials, or Podman internals. |
You already know what a container is. The question behind Podman vs Docker is narrower: can you swap engines without breaking scripts, Compose files, and CI jobs — and which tool fits the way your team actually runs Linux?
Docker popularized docker run, Dockerfiles, and Compose. Podman targets the same OCI images but uses a different process model on the host, pushes rootless operation, and ships first-class systemd integration through Quadlet.
Podman and Docker overlap heavily, but they are not behaviorally identical. The better choice depends on the workload and surrounding tooling — not a single universal winner.
Podman vs Docker: quick comparison
| Area | Docker Engine (typical Linux) | Podman |
|---|---|---|
| Architecture | Client → dockerd → containerd → OCI runtime |
CLI → libpod → conmon → OCI runtime |
| Daemon required | Yes — dockerd for normal operation |
No central daemon for local CLI work |
| Rootless | Supported via rootless dockerd setup |
First-class; common on Fedora and RHEL |
| CLI compatibility | docker subcommands |
Matching podman subcommands; optional docker shim |
| Image format | OCI / Docker v2 | Same registries and tags |
| Compose | docker compose plugin common |
podman compose wraps an external provider |
| Pods | Not a native object | Native podman pod |
| Networking | Docker bridge and embedded DNS | Netavark, aardvark-dns, pasta (rootless) |
| Image builds | Docker BuildKit ecosystem | podman build (Buildah underneath) |
| systemd services | Custom units or compose stack | Quadlet .container units |
| API / socket | /var/run/docker.sock by default |
Optional podman.socket / podman system service |
| Third-party tools | Broad Docker ecosystem | Strong OCI overlap; test socket-dependent tools |
Use the table as a checklist for migration planning, not a scorecard. Docker Desktop on macOS and Windows remains a common developer path even when servers standardize on Podman.
Architecture: daemon vs daemonless
Docker's classic Linux stack routes most work through a persistent daemon:
docker CLI
↓
dockerd
↓
containerd / OCI runtime
↓
containerThe client sends API requests to dockerd. That daemon owns container state, serializes operations, and often runs as root even when you only list images.
Podman's usual local workflow looks like this:
podman CLI
↓
libpod
↓
conmon / OCI runtime
↓
containerEach podman invocation loads libpod in-process, prepares storage and configuration, and fork/exec's the runtime. Detached containers keep running after the CLI exits. There is no dockerd-equivalent broker in the middle for day-to-day commands.
Important nuance — avoid oversimplifying:
- Docker does support rootless operation through rootless
dockerd, separate from the default package install. - Podman can expose a Docker-compatible API through
podman system serviceor apodman.socketunit when remote clients need a socket. - Neither "Docker always needs root" nor "Podman never runs background services" is accurate.
On the lab host, Docker Engine was not installed — only Podman answered container commands:
systemctl is-active docker 2>/dev/null || echo "docker service not active"Sample output:
inactive
docker service not activeProcess trees, storage layout, and Netavark internals belong in Podman architecture, not here.
Podman and Docker command compatibility
Podman deliberately mirrors Docker's verb structure:
docker run ↔ podman run
docker ps ↔ podman ps
docker images ↔ podman images
docker exec ↔ podman exec
docker build ↔ podman build
docker logs ↔ podman logs
docker pull ↔ podman pull
docker push ↔ podman pushSimilar syntax does not guarantee identical behavior. Edge-case flags, default networks, volume SELinux labels, and restart semantics can diverge. Always re-test scripts on a staging host instead of assuming drop-in parity.
Shim, socket, and API compatibility
Three patterns matter for automation:
| Pattern | What it does |
|---|---|
podman-docker package |
Installs /usr/bin/docker as a wrapper that calls Podman |
| Shell alias | alias docker=podman on distributions without the package |
podman system service |
Exposes a Docker-compatible REST API for clients that expect a socket |
Tools that read DOCKER_HOST or hard-code /var/run/docker.sock may work after you enable Podman's socket or symlink carefully — or they may break if they depend on dockerd-specific responses. See Podman socket and Docker API compatibility for socket paths, activation, and API testing.
On RHEL, install the shim when legacy playbooks invoke docker by name:
dnf install -y podman-dockerThe wrapper reports that it emulates Docker and forwards to Podman:
docker --versionSample output:
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
podman version 5.8.2That is still Podman under the hood — not Docker Engine. The shim helps command-name compatibility, not dockerd semantics.
Are Docker images compatible with Podman?
Image compatibility is generally high, but runtime behavior involving networking, storage, Compose, sockets, and privileges can differ.
Both engines consume OCI and Docker v2 images from the same registries. Pull a Hub tag with Podman the same way you would with Docker:
podman pull docker.io/library/alpine:latestPodman exits silently on success when the layers already exist locally. List images to confirm the tag landed in local storage:
podman images --format 'table {{.Repository}}\t{{.Tag}}'Sample output:
REPOSITORY TAG
docker.io/library/alpine latestRun the same tag — here through the docker shim after podman-docker is installed:
docker run --rm docker.io/library/alpine:latest echo hello-from-shimSample output:
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
hello-from-shimThe native Podman command produces the same application output:
podman run --rm docker.io/library/alpine:latest echo hello-from-podmanSample output:
hello-from-podmanThis lab did not have Docker Engine installed, so both commands exercised Podman storage. On a host with real dockerd, you would expect the same image digest to run on both engines — but you should still verify networking, volumes, and entrypoint behavior in your environment.
Containerfiles and Dockerfiles share most instructions (FROM, RUN, COPY, ENTRYPOINT). BuildKit-only features and obscure Dockerfile directives are where "it built on Docker" failures appear. Build walkthroughs live in Build images with Podman.
Rootless containers: Podman vs Docker
| Topic | Docker (rootless path) | Podman (typical emphasis) |
|---|---|---|
| Support | dockerd-rootless-setuptool and rootless socket |
Default documentation targets rootless users |
| User namespaces | Used in rootless dockerd | Core to rootless Podman |
| subuid / subgid | Required for mapping | Required; often auto-configured on new accounts |
| Storage | Separate rootless data dir | Per-user paths under $HOME |
| Privileged ports | Restrictions apply | Ports below 1024 need workarounds |
Devices and --privileged |
Limited compared to rootful | Similar limitations rootless |
Docker can operate rootless. Podman markets rootless more prominently on RHEL-family systems and avoids handing every user a shared root daemon socket on default installs.
On the lab host these commands ran as rootful Podman:
podman info --format 'rootless={{.Host.Security.Rootless}}'Sample output:
rootless=falseThat flag describes the current invocation, not Podman's capabilities. A normal user with subuid ranges configured would see rootless=true.
Do not treat rootless as "free security." Volume ownership, bind mounts, and capability drops still need attention. Configuration depth is in rootless Podman and rootless vs rootful Podman.
Docker Compose vs Podman Compose
Compose is a frequent migration blocker — readers search podman vs docker compose explicitly.
| Option | Summary |
|---|---|
docker compose |
Docker's compose plugin or standalone docker-compose, tied to Docker Engine or Desktop |
podman compose |
Podman subcommand that delegates to an external provider |
podman-compose |
Python-based provider some distributions package separately |
| Compose via socket | Some teams point docker compose at Podman's Docker-compatible API |
podman compose is a wrapper, not a full embedded reimplementation. On a minimal RHEL 10 host with only Podman installed, no provider is present until you add one:
podman compose versionSample output:
Error: looking up compose provider failed
7 errors occurred:
* exec: "docker-compose": executable file not found in $PATH
* exec: "podman-compose": executable file not found in $PATH
...Install a supported Compose provider such as docker-compose or podman-compose. If your Compose implementation lives at another executable path, configure it through compose_providers in containers.conf or PODMAN_COMPOSE_PROVIDER, then re-test your compose.yaml. Profiles, secrets, and vendor-specific extensions vary by provider version.
Do not assume every Docker Compose file lifts unchanged. Validate in staging. Full decision paths are in Podman Compose.
Networking differences
Both engines connect containers to networks, publish ports, and resolve names — with different implementations.
| Area | Docker (typical) | Podman (current defaults) |
|---|---|---|
| Backend | docker0 bridge, user-defined bridges |
Netavark |
| DNS on custom networks | Embedded Docker DNS | aardvark-dns |
| Rootless egress | RootlessKit with gvisor-tap-vsock, slirp4netns, pasta, or VPNKit depending on Engine/version/config | pasta by default on current Podman; slirp4netns also available |
| Config | /etc/docker/daemon.json |
/etc/containers/containers.conf, storage.conf |
Migration pain points to plan for:
- network names and IDs differ — automation that hard-codes Docker network names may break
- container DNS resolution paths change when moving from Docker DNS to aardvark-dns
- rootless port publishing and host reachability behave differently than rootful bridge mode
- tools that assume the Docker socket for network plumbing need retesting
Port syntax (-p 8080:80) looks the same; the underlying rules and firewall hooks do not. Command-level networking belongs in Podman networking and port mapping.
Pods and Kubernetes workflows
Docker groups multi-container apps primarily through Compose networks or custom orchestration. Podman adds a pod abstraction — containers that share a network namespace, similar to a Kubernetes Pod on one host.
| Approach | Role |
|---|---|
| Podman pods | Native grouping; shared localhost between containers in the pod |
| Docker Compose | Multi-service apps via compose networks and service names |
podman kube play |
Run Kubernetes YAML locally for tests or edge nodes |
| Kubernetes / OpenShift | Multi-node orchestration — neither Podman nor Docker replaces this |
podman kube play helps you test YAML before a cluster commit. See Run Kubernetes YAML with podman kube play for supported kinds, ignored fields, and local teardown. It does not turn a single Linux box into a production Kubernetes replacement.
Pod details: Podman pods.
Building images: Podman vs Docker
| Podman | Docker | |
|---|---|---|
| Command | podman build |
docker build |
| Engine | Buildah libraries | BuildKit (modern default) |
| File name | Containerfile or Dockerfile |
Dockerfile |
| Caching | Layer cache supported; flags differ | BuildKit cache mounts and advanced features |
| Secrets | Buildah/Podman secret mounts | BuildKit secret mounts |
| CI | Common on RHEL CI images | Common on generic Docker runners |
For most Dockerfiles without exotic BuildKit-only syntax, the same file builds on both tools. Differences appear in cache invalidation, secret syntax, and multi-stage edge cases — test your pipelines instead of trusting parity claims.
systemd and long-running services
Docker servers often rely on docker run --restart, a compose stack under systemd, or an external orchestrator. Podman adds tighter systemd integration on Linux:
- Podman restart policies for standalone containers
podman-restart.servicefor eligible CLI-created containers after host reboot- Quadlet — declarative
.container,.pod,.volume,.network, and other units generated into systemd services
Docker has no first-party Quadlet equivalent. Teams that want systemctl enable --now myapp.service without hand-written wrapper scripts often prefer Podman on RHEL and Fedora.
Quadlet syntax and examples are in Podman Quadlet. A legacy RHEL-focused walkthrough also exists at Podman Quadlet with systemd for readers on older layouts.
Tooling and CI compatibility
| Risk area | What breaks or needs retesting |
|---|---|
Scripts hard-coded to docker |
May work with shim or alias; verify flags |
/var/run/docker.sock assumptions |
Enable Podman socket or refactor clients |
| Compose | Provider install and feature parity |
| Testcontainers | Often expects Docker socket or Testcontainers for Podman config |
| CI images | docker CLI present but Podman-only hosts need pipeline edits |
| Docker API clients | May need podman system service and compatibility testing |
| Docker-only plugins | Swarm, Desktop extensions, legacy --link |
| Privileged workflows | Rootless limits on both engines |
This section stays comparative. Step-by-step cutover checklists are in Migrate from Docker to Podman.
Should you use Podman or Docker?
Choose Podman when
- Linux-first server workloads dominate your estate
- rootless operation per user or service account is a security goal
- systemd and Quadlet integration simplifies service ownership
- native pods or
podman kube playhelp your workflow - you want local container management without a always-on
dockerd
Choose Docker when
- maximum third-party Docker ecosystem compatibility is non-negotiable
- Docker-specific tools, Desktop features, or Swarm are central
- developers depend on Docker Desktop workflows on macOS or Windows
- existing automation is deeply tied to dockerd APIs and socket behavior
Either may work well when
- the requirement is standard OCI image build, run, push, and pull on Linux
- you validate Compose, sockets, and networking in staging before cutover
Many organizations run Docker Desktop locally and Podman in production CI — shared images make that workable until pipelines catch up.
References
- Podman documentation
- Docker Engine overview
- Open Container Initiative
- Podman compose command reference
Summary
Podman and Docker both run OCI containers from the same registries, but they organize host-side work differently. Docker centers on dockerd, a persistent daemon that owns state and serves a socket API. Podman coordinates each CLI invocation through libpod and per-container helpers such as conmon, which changes how rootless users and systemd services interact with workloads.
Command names align closely enough that many teams start with podman run instead of docker run, or install the podman-docker shim for scripts that still call docker. Image pulls from Docker Hub worked on the lab host with both the native CLI and the shim — yet Compose, networking backends, and socket-dependent tools remain the places where "compatible" stops meaning "identical."
Neither tool wins every category. Pick Podman when Linux servers, rootless defaults, and Quadlet matter; keep Docker when Desktop workflows, deep dockerd integration, or niche Docker-only features dominate. If you are evaluating a switch, read What is Podman? for vocabulary, then Migrate from Docker to Podman when you are ready to change pipelines.

