Migrate from Docker to Podman: Commands, Compose and systemd

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:

bash
docker ps -a

Use 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:

bash
docker images

List named volumes next — you will need their names for data export:

bash
docker volume ls

Custom bridge networks affect service DNS during migration:

bash
docker network ls

For Compose projects:

bash
docker compose config

For 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.sock or set DOCKER_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:

bash
podman --version

Sample output:

output
podman version 5.8.2

Check storage paths you will migrate into:

bash
podman info --format 'GraphRoot: {{.Store.GraphRoot}}'
output
GraphRoot: /var/lib/containers/storage

Named volumes land under a separate path — confirm it before planning exports:

bash
podman info --format 'VolumePath: {{.Store.VolumePath}}'
output
VolumePath: /var/lib/containers/storage/volumes

Smoke-test pulls and runtime:

bash
podman run --rm quay.io/podman/hello

A 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:

bash
podman build --build-arg APP_VERSION=2.0 -t localhost/migrate-demo:v1 -f Dockerfile .

Sample output:

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:v1

Verify runtime:

bash
podman run --rm localhost/migrate-demo:v1
output
built 2.0

Podman 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:

bash
docker run -d \
  --name web \
  -p 8080:80 \
  -e APP_ENV=production \
  -v web-data:/data \
  --restart=always \
  IMAGE

Podman equivalent for the same flags:

bash
podman run -d \
  --name migrate-web \
  -p 8090:80 \
  -e APP_ENV=production \
  -v migrate-data:/data \
  --restart=always \
  docker.io/library/nginx:latest

Confirm the container and restart policy:

bash
podman ps --filter name=migrate-web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
output
NAMES        STATUS       PORTS
migrate-web  Up 1 second  0.0.0.0:8090->80/tcp

The restart policy should match what you had under Docker:

bash
podman inspect migrate-web --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}}'
output
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:

bash
sudo systemctl enable podman-restart.service

After a host reboot, confirm the service ran and the container came back:

bash
systemctl status podman-restart.service --no-pager
output
● 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)
bash
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/
output
NAMES        STATUS         PORTS
migrate-web  Up 58 seconds  0.0.0.0:8090->80/tcp
HTTP 200

Podman 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:

ini
[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.target

Quadlet 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:

bash
systemctl is-active migrate-web-q.service
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8091/
output
active
HTTP 200

Migrate Docker Compose

Do not pick one Compose path blindly. Three options exist:

  1. podman compose — Podman wrapper around an external provider
  2. podman-compose — Python implementation calling Podman directly
  3. docker compose with DOCKER_HOST pointed at podman.socket

Decision tables, provider precedence, network compat flags, and pod-mode behavior are in Podman Compose. Here the migration step is:

bash
docker compose config

Render 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_on and health conditions
  • secrets and profiles
  • build sections 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:

bash
systemctl --user enable --now podman.socket

For rootless API availability when the user is not logged in, enable lingering:

bash
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:

bash
export DOCKER_HOST="unix://${XDG_RUNTIME_DIR}/podman/podman.sock"

Test with the Docker CLI shim:

bash
docker ps

Sample 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:

text
Docker named volume
export application data (tar, db dump, rsync)
podman volume create on target
restore into new volume

Create the Podman volume:

bash
podman volume create migrate-data

Seed data through a temporary container:

bash
podman run --rm -v migrate-data:/data docker.io/library/alpine:latest sh -c 'echo seed-data > /data/app.conf'

Verify:

bash
podman run --rm -v migrate-data:/data docker.io/library/alpine:latest cat /data/app.conf
output
seed-data

For 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:

text
-v /srv/app:/data

On 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:

bash
docker network create app-net

Podman:

bash
podman network create migrate-net

Check the subnet:

bash
podman network inspect migrate-net --format 'Subnet={{(index .Subnets 0).Subnet}}'
output
Subnet=10.89.0.0/24

Then attach containers:

bash
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:

text
docker swarm
docker service
docker stack
docker node

If 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:

bash
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:

bash
docker run --gpus all IMAGE

Podman CDI style (NVIDIA's documented Podman examples include --security-opt=label=disable):

bash
podman run --rm \
  --device nvidia.com/gpu=all \
  --security-opt=label=disable \
  IMAGE

NVIDIA'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:

bash
nvidia-ctk cdi list

This 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_HOST be configured?
  • Does it require a dockerd-specific API response?
  • Does it shell out to docker instead 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:

text
docker
docker.sock
DOCKER_HOST
docker compose
docker buildx
--gpus
docker login
docker push

Classify 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:

bash
docker build -t app:v1 .

with:

bash
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 logs or journald
  • restart after process failure (--restart or systemd)
  • restart after host reboot (podman-restart.service for --restart containers, 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

IMPORTANT
Copying /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:

bash
sudo dnf install podman-docker

Verify:

bash
docker --version

Sample output:

output
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
podman version 5.8.2

List containers through the shim:

bash
docker ps

On this host, podman-docker linked the traditional Docker socket path to the rootful Podman socket:

text
/var/run/docker.sock -> /run/podman/podman.sock

That 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

text
[ ] 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 cutover

Troubleshooting

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


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.


Frequently Asked Questions

1. Can I migrate by copying /var/lib/docker to Podman storage?

No. Docker and Podman use different storage metadata layouts. Migrate images through a registry or podman save and load, application data through volume export and restore, and container configuration through documented commands, Compose, or Quadlet — not by copying internal daemon directories.

2. Does Podman support docker run --restart always?

Yes for container exits — podman run accepts --restart policies such as always. Host-reboot recovery is separate: Podman relies on podman-restart.service for containers started with --restart, and Quadlet with systemd Restart= is the stronger production pattern for managed services on Enterprise Linux.

3. What happens to Docker Compose files on Podman?

Compose YAML can be run with podman compose, which delegates to an installed provider such as Docker Compose or podman-compose, or by configuring Docker Compose directly against the Podman socket. Provider choice affects networking, generated names, and feature support — test the exact project on staging before cutover.

4. Is there a Podman equivalent to Docker Swarm?

No. docker swarm, docker service, docker stack, and docker node have no Podman orchestration counterpart. Swarm-dependent workloads need a separate orchestration migration such as Kubernetes or OpenShift.

5. How do I migrate Docker named volumes to Podman?

Export application data from the Docker volume with a temporary container or database-aware backup, create a new Podman volume with podman volume create, then restore data into that volume. Do not move files from /var/lib/docker/volumes into Podman internal storage paths.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)