Choosing Between Docker and Podman on CentOS Stream 9: A Decision Guide
A practical decision guide for selecting Docker or Podman on CentOS Stream 9, covering architecture differences, security defaults, systemd integration, ecosystem compatibility, and a hands-on validation procedure.
15 Mar 2026, 14:09 UTC

The Decision: Container Runtime for a New CentOS Stream 9 Application Server
You are provisioning a new application server on CentOS Stream 9 and must choose the container runtime. The decision affects security posture, operational overhead, ecosystem compatibility, and long-term maintenance. Both Docker and Podman are viable, but they differ fundamentally in architecture, default security model, and integration with the OS.
Constraints That Shape the Choice
- OS baseline: CentOS Stream 9 ships Podman 4.x in the base repositories; Docker requires the upstream Docker repository.
- Security policy: Rootless containers are preferred to limit privilege escalation.
- Operational model: systemd-native service management is expected for container workloads.
- Team familiarity: Existing CI/CD scripts use Docker CLI syntax and
docker-compose.ymlfiles. - Support requirements: Enterprise support contracts may dictate a specific vendor.
Supported Options Compared
| Criterion | Docker Engine (CE) | Podman (Default on CentOS Stream 9) |
|---|---|---|
| Architecture | Client–daemon (dockerd runs as root) |
Daemonless; each command runs directly |
| Rootless default | Optional, requires explicit setup | Enabled by default for non-root users |
| systemd integration | Via docker.service; containers managed through daemon |
Native systemd units per container (podman generate systemd) |
| Image build | docker build |
podman build (identical syntax, uses Buildah internally) |
| Compose support | Built-in docker compose (v2 plugin) |
podman compose (wraps Docker Compose v2) or docker-compose via alias |
| Repository source | Docker upstream repo (manual add) | CentOS BaseOS / AppStream (dnf install podman) |
| SELinux integration | Requires container-selinux; daemon runs in container_runtime_t |
Tight integration; rootless processes confined by user SELinux policy |
| Enterprise support | Docker Inc. subscriptions, Mirantis, cloud vendor offerings | Red Hat subscription (includes Podman, Buildah, Skopeo) |
Trade-offs
Security and Attack Surface
Podman’s daemonless model eliminates the persistent root daemon that Docker runs. Each podman invocation executes in the caller’s context, so a compromised container cannot pivot to a central daemon. Rootless mode is the default for non-root users, mapping container UID 0 to an unprivileged host UID via user namespaces. Docker can run rootless, but it requires additional configuration (dockerd-rootless-setuptool.sh) and a separate systemd user service.
Operational Simplicity
On CentOS Stream 9, dnf install podman pulls a fully supported, SELinux-aware build with no extra repositories. Updates arrive through the standard OS patch cycle. Docker demands adding the Docker CE repository, importing GPG keys, and tracking a separate update stream. For teams already using Ansible or dnf-automatic, Podman fits the existing patch workflow.
Ecosystem Compatibility
Docker Compose v2 is a Go plugin distributed with Docker Desktop and the Linux CLI plugin. Podman provides podman compose which shells out to the same Compose v2 binary if present, or you can install the standalone docker-compose plugin and alias docker-compose to podman compose. Most existing docker-compose.yml files work unchanged. Kubernetes YAML generation (podman kube generate) is a Podman-specific advantage for teams moving toward OpenShift or vanilla K8s.
Resource Overhead
A Docker daemon consumes ~50–100 MB RSS plus per-container shim processes. Podman spawns short-lived processes only when commands run; idle containers have no extra supervisor. On a server hosting dozens of small services, this difference can reclaim hundreds of megabytes.
Potential Friction Points
- SELinux denials in rootless mode: Certain workloads (e.g., mounting host paths with
:Zlabels, using NFS volumes) may require custom policies orsetsebool -P container_manage_cgroup on. - Docker daemon conflict: Running both
dockerdand Podman on the same host can cause cgroup v2 delegation issues. Pick one. - Licensing: Docker Desktop requires a paid license for enterprises >250 employees or >$10M revenue; the Linux CLI/engine remains Apache 2.0. Podman is fully open source (Apache 2.0) with Red Hat support via subscription.
Concrete Validation: Build and Run a Test Workload
Run the following steps on a fresh CentOS Stream 9 VM (or container) to verify behavior before committing. Commands assume a non-root user with sudo access.
1. Install Both Runtimes (for comparison only; choose one for production)
# Podman (default repo)
sudo dnf install -y podman
# Docker (upstream repo)
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
2. Verify Versions and Architecture
podman version --format '{{.Client.Version}}'
docker version --format '{{.Server.Version}}'
# Check daemon presence
systemctl status docker # shows active dockerd
systemctl status podman # shows "Unit podman.service could not be found" (expected)
Expected: Podman reports client version only; Docker shows client and server versions. systemctl status podman returns "not found" because Podman has no system service.
3. Build an Identical Image with Both Tools
cat > Dockerfile <<'EOF'
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.4
RUN microdnf install -y httpd && microdnf clean all
EXPOSE 8080
CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"]
EOF
podman build -t localhost/test-podman:latest .
docker build -t localhost/test-docker:latest .
Both commands should succeed with identical layer output. Podman stores images in ~/.local/share/containers/storage (rootless) or /var/lib/containers/storage (root). Docker uses /var/lib/docker.
4. Run Containers and Compare Resource Usage
# Podman rootless
podman run -d --name web-podman -p 8080:8080 localhost/test-podman:latest
# Docker (requires user in docker group or sudo)
docker run -d --name web-docker -p 8081:8080 localhost/test-docker:latest
# Check memory footprint
ps -o pid,rss,cmd -C httpd -C docker-proxy -C conmon
Observe that Podman’s conmon monitor runs per container; Docker runs docker-proxy per published port plus the daemon. Use podman top web-podman and docker top web-docker to inspect processes inside each container.
5. Generate systemd Unit for Podman Container
podman generate systemd --name web-podman --files --new
# Creates container-web-podman.service in cwd
sudo cp container-web-podman.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now container-web-podman.service
systemctl status container-web-podman.service
The service starts the container on boot, handles restart policies, and integrates with journalctl -u container-web-podman. Docker containers typically rely on --restart=unless-stopped or a separate systemd unit that wraps docker start.
6. Test Compose Compatibility
cat > compose.yml <<'EOF'
services:
web:
image: localhost/test-podman:latest
ports:
- "8082:8080"
EOF
podman compose -f compose.yml up -d
podman compose -f compose.yml ps
podman compose -f compose.yml down
If podman compose is not installed, dnf install podman-compose (Python wrapper) or install the Docker Compose v2 plugin and ensure docker compose is on PATH; Podman will delegate to it.
Verification Checklist Before Production
- Image pull/push:
podman login registry.example.com && podman push localhost/test-podman:latest registry.example.com/app:1.0 - Rootless port binding: Confirm
sysctl net.ipv4.ip_unprivileged_port_start=8080or usesetcap CAP_NET_BIND_SERVICE=+epon the binary if binding < 1024. - SELinux labeling: Run
podman run -v /host/path:/container/path:Z ...and verify no AVC denials inausearch -m avc -ts recent. - Log rotation: Configure
/etc/containers/containers.conf[containers]log_driver = "journald"andlog_size_max = "10m". - Backup/restore: Test
podman save -o image.tar localhost/test-podman:latestandpodman load -i image.tar.
Limitations and When to Re-evaluate
- If your organization mandates Docker Enterprise support contracts, Podman may not satisfy vendor requirements.
- Workloads requiring Docker-specific plugins (e.g., certain volume drivers, legacy Swarm clusters) need Docker Engine.
- Rootless Podman on NFS home directories can hit ID-mapping limits; consider
/etc/subuid//etc/subgidranges andpodman unsharefor image builds. - CentOS Stream 9’s Podman version (4.x) may lag upstream by months; if you need bleeding-edge features (e.g.,
podman machinefor Mac/Windows parity), evaluate the COPR or upstream builds.
Decision Summary
For a greenfield CentOS Stream 9 server where rootless security, systemd integration, and OS-native updates are priorities, Podman is the lower-friction default. Choose Docker only if enterprise support contracts, Swarm, or specific Docker-only plugins are non-negotiable. The validation steps above take ~15 minutes and confirm the runtime behaves as expected in your environment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.