| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64 (staging); podman-docker-5.8.2-5.el10_2.x86_64 (cutover shim, tested separately) |
| Applies to | Linux hosts migrating from Docker Engine or Docker CLI workflows to Podman |
| Privilege | Rootful and rootless examples; Quadlet unit files and system socket paths may need administrator access |
| Scope | Practical Docker-to-Podman migration — workload inventory, CLI translation table, podman-docker shim, Dockerfile and podman build, docker run equivalents and restart policy, Quadlet conversion sketch, Compose provider choice, socket paths, named-volume data migration, bind mounts and SELinux, networks, Swarm gap, secrets recreation, CDI GPU notes, API consumer checklist, CI search patterns, and final cutover checklist. Does not cover Podman-vs-Docker opinion pieces, full Compose or socket tutorials, complete Quadlet course, or Kubernetes-from-Swarm migration. |
| Related guides | Podman vs Docker |
Replacing docker with podman in scripts is the easy part. The hard part is knowing which Docker features your workload actually depends on — Compose provider behavior, socket consumers, Swarm, GPU flags, volume layout, and restart semantics do not map one-for-one.
This guide is a migration workflow: inventory what you have, validate Podman alongside Docker where possible, translate configuration deliberately, and cut over only after runtime behavior matches what production expects.
What changes when you migrate
| Docker | Podman |
|---|---|
docker run |
podman run |
docker ps |
podman ps |
docker build |
podman build |
docker pull / docker push |
podman pull / podman push |
docker volume |
podman volume |
docker network |
podman network |
Docker daemon (dockerd) |
No persistent daemon required for CLI operations |
| Docker API socket | Optional podman.socket |
| Docker Compose | podman compose, podman-compose, or Docker Compose via socket |
| Daemon restart policies | podman run --restart or systemd / Quadlet |
| Docker Swarm | No direct Podman equivalent |
docker run --gpus |
CDI devices, e.g. --device nvidia.com/gpu=all |
| Docker Swarm secrets | Podman secrets or Compose-provider handling |
docker as a system service |
systemd + Quadlet for managed workloads |
Do not start migration by aliasing docker=podman and assuming the job is finished. First list which rows in that table your applications actually touch.
Inventory the existing Docker workload
Record what is running before you install or replace anything.
List containers:
docker ps -aUse the real Docker Engine CLI for this inventory while Docker is still the production runtime — not the podman-docker shim, which would show Podman-managed containers instead.
Capture images, volumes, and networks the same way:
docker imagesList named volumes next — you will need their names for data export:
docker volume lsCustom bridge networks affect service DNS during migration:
docker network lsFor Compose projects:
docker compose configFor daemon-level settings, note /etc/docker/daemon.json if present.
Write down:
- image names and tags
- published ports and environment variables
- named volumes and bind mounts
- custom networks, aliases, and DNS expectations
- restart policy and health checks
- capabilities, devices, and GPU flags
- Compose files and profiles
- tools that read
docker.sockor setDOCKER_HOST - logging drivers
- CI job definitions
- Swarm, stack, or service usage
Do not migrate until this inventory exists. You cannot validate what you have not documented.
Install Podman alongside Docker first
Run Podman in parallel with Docker on important hosts until staging proves parity.
Confirm the package:
podman --versionSample output:
podman version 5.8.2Check storage paths you will migrate into:
podman info --format 'GraphRoot: {{.Store.GraphRoot}}'GraphRoot: /var/lib/containers/storageNamed volumes land under a separate path — confirm it before planning exports:
podman info --format 'VolumePath: {{.Store.VolumePath}}'VolumePath: /var/lib/containers/storage/volumesSmoke-test pulls and runtime:
podman run --rm quay.io/podman/helloA successful run prints the Podman hello banner. Avoid uninstalling Docker Engine on a host still serving production traffic until cutover tests pass.
During this parallel phase, install podman only. Do not install podman-docker yet — that package provides a docker command that calls Podman and, on RHEL, provides the traditional Docker socket path as a link to the rootful Podman socket (/var/run/docker.sock → /run/podman/podman.sock). That link does not point at a rootless user's ${XDG_RUNTIME_DIR} socket. While you still need the real Docker CLI and socket for comparison, podman-docker can replace the Docker-facing interface you are trying to validate against.
Docker CLI to Podman CLI
Common translations:
| Docker | Podman |
|---|---|
docker run |
podman run |
docker create |
podman create |
docker ps |
podman ps |
docker exec |
podman exec |
docker cp |
podman cp |
docker rm |
podman rm |
docker images |
podman images |
docker rmi |
podman rmi |
docker build |
podman build |
docker inspect |
podman inspect |
docker logs |
podman logs |
docker stats |
podman stats |
docker volume |
podman volume |
docker network |
podman network |
docker login |
podman login |
docker save / docker load |
podman save / podman load |
Similar command names do not guarantee identical defaults. Validate networking, rootless UID mapping, SELinux labels on mounts, Compose provider behavior, and API integrations on your exact files — not from this table alone.
Dockerfile to Containerfile
Most Dockerfiles build unchanged:
podman build --build-arg APP_VERSION=2.0 -t localhost/migrate-demo:v1 -f Dockerfile .Sample output:
STEP 3/4: RUN echo "built ${APP_VERSION}" > /version.txt
STEP 4/4: CMD ["cat", "/version.txt"]
COMMIT localhost/migrate-demo:v1
Successfully tagged localhost/migrate-demo:v1Verify runtime:
podman run --rm localhost/migrate-demo:v1built 2.0Podman also recognizes Containerfile as a filename. Do not rename Dockerfile merely for branding — test build args, secrets, multi-stage builds, and mounts. BuildKit-only syntax may need changes. Deeper build coverage lives in Podman Containerfile and Build images with Podman.
Migrate docker run
Docker example:
docker run -d \
--name web \
-p 8080:80 \
-e APP_ENV=production \
-v web-data:/data \
--restart=always \
IMAGEPodman equivalent for the same flags:
podman run -d \
--name migrate-web \
-p 8090:80 \
-e APP_ENV=production \
-v migrate-data:/data \
--restart=always \
docker.io/library/nginx:latestConfirm the container and restart policy:
podman ps --filter name=migrate-web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"NAMES STATUS PORTS
migrate-web Up 1 second 0.0.0.0:8090->80/tcpThe restart policy should match what you had under Docker:
podman inspect migrate-web --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}}'RestartPolicy=always--restart=always controls restart behavior when the container exits, but host-reboot recovery is a separate concern. Podman documents reboot-time restart through podman-restart.service for containers started with --restart. For services whose lifecycle is managed by systemd, prefer Quadlet with [Service] Restart= instead of combining systemd management with --restart — Docker daemon restart semantics do not carry over automatically.
Enable the reboot helper once per host:
sudo systemctl enable podman-restart.serviceAfter a host reboot, confirm the service ran and the container came back:
systemctl status podman-restart.service --no-pager● podman-restart.service - Podman Start All Containers With Restart Policy Set To Always
Loaded: loaded (/usr/lib/systemd/system/podman-restart.service; enabled; preset: disabled)
Active: active (exited) since Sun 2026-08-23 15:02:37 IST; 51s ago
Process: 1294 ExecStart=/usr/bin/podman start --all --filter should-start-on-boot=true (code=exited, status=0/SUCCESS)podman ps --filter name=migrate-web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8090/NAMES STATUS PORTS
migrate-web Up 58 seconds 0.0.0.0:8090->80/tcp
HTTP 200Podman does support --restart on standalone containers. The migration decision is whether an interactive podman run or a declarative systemd unit is the right lifecycle model for a server workload.
Convert long-running services to Quadlet
For hosts that should start containers at boot and integrate with journald, translate imperative docker run into a Quadlet .container unit:
[Container]
Image=docker.io/library/nginx:latest
ContainerName=migrate-web-q
PublishPort=8091:80
Environment=APP_ENV=production
Volume=migrate-data:/data
[Service]
Restart=always
[Install]
WantedBy=multi-user.targetQuadlet gives you systemd dependencies, declarative configuration, and optional auto-update hooks — stronger than a one-off --restart for production services. Unit layout, dependencies, and troubleshooting are covered in Podman Quadlet and its chapter articles; this page only shows the translation shape.
On the RHEL 10 lab host, migrate-web-q started automatically after reboot through WantedBy=multi-user.target — no manual systemctl start:
systemctl is-active migrate-web-q.service
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8091/active
HTTP 200Migrate Docker Compose
Do not pick one Compose path blindly. Three options exist:
podman compose— Podman wrapper around an external providerpodman-compose— Python implementation calling Podman directlydocker composewithDOCKER_HOSTpointed atpodman.socket
Decision tables, provider precedence, network compat flags, and pod-mode behavior are in Podman Compose. Here the migration step is:
docker compose configRender the effective project, then run the same file through your chosen Podman provider on staging. Pay attention to:
- default network selection and service DNS
- generated container name separators
depends_onand health conditions- secrets and profiles
buildsections and GPU device settings
Docker socket to Podman socket
API clients expect a Unix socket. Typical paths:
| Mode | Socket |
|---|---|
| Podman rootful | unix:///run/podman/podman.sock |
| Podman rootless | unix://${XDG_RUNTIME_DIR}/podman/podman.sock |
Legacy Docker path (rootful, via podman-docker on RHEL) |
/var/run/docker.sock → /run/podman/podman.sock |
Enable the rootless user socket persistently and start it now:
systemctl --user enable --now podman.socketFor rootless API availability when the user is not logged in, enable lingering:
sudo loginctl enable-linger <USER>Podman documents both socket enablement and user lingering for persistent rootless socket activation.
Point Docker-compatible clients at the rootless Podman API socket:
export DOCKER_HOST="unix://${XDG_RUNTIME_DIR}/podman/podman.sock"Test with the Docker CLI shim:
docker psSample output shows Podman-managed containers — not Docker Engine. Do not create global compatibility symlinks until you know which tools require them and whether they need rootful or rootless context.
Migrate Docker named volumes
Do not copy /var/lib/docker/volumes/* into /var/lib/containers/storage/volumes/*. Docker and Podman store different metadata around the same application bytes.
Safe pattern:
Docker named volume
↓
export application data (tar, db dump, rsync)
↓
podman volume create on target
↓
restore into new volumeCreate the Podman volume:
podman volume create migrate-dataSeed data through a temporary container:
podman run --rm -v migrate-data:/data docker.io/library/alpine:latest sh -c 'echo seed-data > /data/app.conf'Verify:
podman run --rm -v migrate-data:/data docker.io/library/alpine:latest cat /data/app.confseed-dataFor databases, quiesce the application and prefer database-native backup tools — a plain tar of live files is not transaction-consistent for every engine.
Migrate bind mounts
Bind syntax is identical:
-v /srv/app:/dataOn SELinux-enforcing hosts, wrong labels cause permission errors even when Unix mode bits look correct. Use :z or :Z only after you understand shared versus private labeling — see Fix Podman volume permissions. Do not add :Z to every host path by default.
Migrate container networks
Docker custom bridge:
docker network create app-netPodman:
podman network create migrate-netCheck the subnet:
podman network inspect migrate-net --format 'Subnet={{(index .Subnets 0).Subnet}}'Subnet=10.89.0.0/24Then attach containers:
podman run --network migrate-net ...Validate DNS between services, rootless versus rootful context, subnet collisions with existing Docker bridges, and any hard-coded gateway assumptions. Default bridge details from Docker do not transfer literally.
Docker Swarm has no Podman equivalent
These Docker commands do not map to Podman orchestration:
docker swarm
docker service
docker stack
docker nodeIf production depends on Swarm scheduling, overlay networks, or stack deploy, you are migrating orchestration — not just the container CLI. Destinations include Kubernetes or OpenShift. Podman pods group local containers; they are not a multi-node scheduler and are not a Swarm replacement.
Docker secrets
Docker Swarm secrets and Podman secrets are different systems. Recreate secret values through the target mechanism:
podman secret create ...Compose secret stanzas behave differently per provider and version. Do not copy Swarm secret storage paths. See Manage Podman secrets for create, mount, and env workflows.
Docker --gpus to Podman CDI
Modern NVIDIA workflows on Podman use CDI instead of Docker's --gpus flag.
Docker style:
docker run --gpus all IMAGEPodman CDI style (NVIDIA's documented Podman examples include --security-opt=label=disable):
podman run --rm \
--device nvidia.com/gpu=all \
--security-opt=label=disable \
IMAGENVIDIA's documented Podman CDI examples include --security-opt=label=disable; on SELinux-enforcing hosts this may be required for GPU device access, depending on the installed policy and configuration. If your site policy forbids label=disable, work with your security team on an alternative labeling approach.
Prerequisite: NVIDIA Container Toolkit generates CDI specs — verify with:
nvidia-ctk cdi listThis host has no NVIDIA GPU or CDI configuration — the command is documented for migration planning, not validated here. Test GPU workloads on hardware that matches production before cutover.
Migrate Docker API consumers
Audit tools that may use docker.sock or the Docker API:
- Docker Compose and Testcontainers
- CI runners and image scanners
- Monitoring agents and language SDKs
- Local development frameworks
For each consumer, ask:
- Can the socket URL or
DOCKER_HOSTbe configured? - Does it require a dockerd-specific API response?
- Does it shell out to
dockerinstead of using the API? - Does it assume Docker-only image or container metadata?
- Does it need privileged socket access?
Speaking the Docker API is not the same as full compatibility with every dockerd behavior.
Check CI before switching
Search pipelines for:
docker
docker.sock
DOCKER_HOST
docker compose
docker buildx
--gpus
docker login
docker pushClassify each hit:
| Class | Migration effort |
|---|---|
CLI-only (docker build, docker run) |
Usually straightforward — swap to podman or keep docker via podman-docker |
| API / socket | Test with podman.socket and documented DOCKER_HOST |
| BuildKit / buildx | Validate podman build feature parity |
| Registry auth | podman login — same pattern, different config path |
| Orchestration (Swarm, stack) | Out of scope for CLI migration |
| GPU / devices | CDI setup and hardware validation |
CLI-only jobs are the easiest wins. BuildKit, socket, and Swarm integrations need staged testing.
Validate image builds
Compare:
docker build -t app:v1 .with:
podman build -t app:v1 .Check final filesystem contents, ENTRYPOINT / CMD, user ID, exposed ports, labels, architecture, health checks, and application startup. Image IDs may differ between engines even when content is equivalent — compare behavior, not digests alone.
Validate runtime behavior
For each migrated service confirm:
- container starts and stays running
- published ports answer on the host
- persistent data is present after restart
- service DNS works on Compose networks
- logs are visible through
podman logsor journald - restart after process failure (
--restartor systemd) - restart after host reboot (
podman-restart.servicefor--restartcontainers, or Quadlet / systemd) - rootless permissions on bind mounts
- health check endpoints if configured
Migration is not complete because podman run returned a container ID once.
Do not copy Docker internal storage
/var/lib/docker into /var/lib/containers/storage is not a supported migration path. You will not preserve usable image, volume, or container metadata. Export application data and images through supported commands instead.
Transfer:
- images through a registry or
docker save/podman load - volume data through export and restore into new Podman volumes
- configuration through
podman run, Compose, or Quadlet units
Install podman-docker at cutover
After staging proves Podman parity and you no longer need the original Docker CLI or socket on that host, install the compatibility shim on RHEL-family systems. podman-docker provides a docker CLI that executes Podman:
sudo dnf install podman-dockerVerify:
docker --versionSample output:
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
podman version 5.8.2List containers through the shim:
docker psOn this host, podman-docker linked the traditional Docker socket path to the rootful Podman socket:
/var/run/docker.sock -> /run/podman/podman.sockThat bridge helps scripts and tools that hard-code /var/run/docker.sock against the rootful Podman API. It does not make that path point at a rootless user's ${XDG_RUNTIME_DIR} socket. It is a compatibility layer — not the final architecture for every system. See Podman socket and Docker API compatibility for socket paths, DOCKER_HOST, and API testing beyond a simple alias.
Final migration checklist
[ ] Workload inventory documented
[ ] Images available to Podman (pull, save/load, or rebuild)
[ ] Containers recreated with matching ports, env, mounts
[ ] Named volume data exported and restored
[ ] Bind mounts and SELinux labels verified
[ ] Networks and service DNS verified
[ ] Compose tested on chosen provider
[ ] Docker API consumers tested against podman.socket
[ ] Restart and boot behavior verified (--restart + podman-restart.service, or Quadlet)
[ ] Secrets recreated in Podman or Compose
[ ] GPU / CDI tested on target hardware if applicable
[ ] CI jobs updated and green on staging
[ ] Swarm dependency resolved separately if present
[ ] podman-docker installed only after Docker CLI/socket no longer needed
[ ] Docker removed only after successful cutoverTroubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
docker works but shows emulation message |
podman-docker shim active |
Expected on RHEL; create /etc/containers/nodocker to quiet or call podman directly |
Tool cannot find /var/run/docker.sock |
Socket not enabled or wrong mode | Enable podman.socket; symlink via podman-docker or set DOCKER_HOST |
| Volume data missing after copy | Copied Docker metadata paths | Export application data; recreate Podman volume |
docker swarm commands fail |
No Swarm in Podman | Plan orchestration migration separately |
| GPU container fails | CDI not configured or SELinux label block | Install NVIDIA toolkit; verify nvidia-ctk cdi list; on RHEL try --security-opt=label=disable per NVIDIA CDI docs |
References
- Podman documentation — migration overview — upstream command reference
- Red Hat — Building, running, and managing containers — RHEL container workflows
- Compose specification — portable compose file format
- NVIDIA Container Toolkit — CDI — GPU device injection
Summary
Docker-to-Podman migration is a workload audit first and a command swap second. Inventory containers, volumes, networks, Compose files, socket consumers, and Swarm usage before you touch production. Install Podman alongside Docker, run staging tests, and only remove Docker after behavior matches.
CLI translation covers most single-container workflows — podman run, podman build, and the podman-docker shim on RHEL can keep docker scripts working while /var/run/docker.sock points at the rootful Podman socket. Named volumes migrate as application data, not as copies of /var/lib/docker/volumes. Bind mounts keep the same -v syntax but may need SELinux labels on enforcing hosts.
Compose requires an explicit provider choice; long-running servers benefit from Quadlet plus systemd rather than a lone --restart flag. Swarm has no Podman counterpart, and GPU workloads move from --gpus to CDI device strings on hardware you must test yourself. Work through the checklist on staging, then cut over CI and production one pipeline at a time.

