Docker to Podman Migration Differences
`docker run` mostly just works. What breaks is everything around it: image names gaining a `localhost/` prefix, `NetworkSettings.IPAddress` coming back empty, source IPs reading 169.254.1.2, and `--read-only` silently killing a published port.
Security and Operations Guide 47 of 47 Intermediate
- OSUbuntu 26.04 LTS (resolute)
- Podman5.7.0
- Runtimecrun 1.21
- Networknetavark 1.16.1
- TimeAbout 15 min
- Reviewed22 August 2026
Written against the versions above. Podman follows the distribution here rather than a vendor repository, so the version you get is the one Ubuntu shipped. The commands are stable across 5.x.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| PODMAN01 | 192.168.0.24 | Ubuntu 26.04 LTS | Primary Container Host | 2 Core | 4 GB | 50 GB |
Before you start
- Working knowledge of Docker. This guide is only useful as a comparison.
-
What needs no work at all
Start with the good news, because it is most of the surface.
run,ps,stop,rm,exec,logs,build,pull,push,cp,inspect,--formattemplates,-v,-p,-e,--restart, Dockerfiles unchanged, anddockeritself via thepodman-dockershim (guide 4). A Makefile full ofdockercommands generally runs untouched.Even the API works - the socket in guide 70 runs the genuine Docker Compose binary.
So this is not a rewrite. It is a list of specific things that behave differently, and the rest of this guide is that list.
bash Example session docker --versionEmulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.podman version 5.7.0docker psEmulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESExpected result
podman version 5.7.0from adockercommand, and an empty container list.Success conditionYou have Docker commands running under Podman.
-
Things that will break a script
In rough order of how likely they are to bite:
1. Image names gain
localhost/.podman build -t app:1produceslocalhost/app:1. Anything greppingpodman imagesfor^appfinds nothing, and a push needs a re-tag first (guide 30).2. Bare image names resolve differently, or not at all. No implicit Docker Hub.
hello-worldresolves to *Podman's* image on quay.io, and an unaliased name fails outright (guide 12). Fully qualify every image reference - this is the single highest-value change to make.3.
NetworkSettings.IPAddressis empty. Rootless containers on the default network have no bridge address at all (guide 53). Anything reading that field needs a user-defined network.4. The default network has no DNS. Containers cannot resolve each other until you create a network (guide 52).
5. Ports below 1024 fail.
-p 80:80needs a sysctl or a proxy (guide 51).6.
docker infofields do not exist. NoServerVersion, because there is no server. Anything parsinginfooutput needs rewriting (guide 4).bash Example session podman images appREPOSITORY TAG IMAGE ID CREATED SIZElocalhost/app 1 c154b5c3995c Less than a second ago 13.8 MBpodman pull traefikError: short-name "traefik" did not resolve to an alias and no unqualified-search registries are defined in "/etc/containers/registries.conf"[exit 125]podman inspect alpha --format 'ip=[{{.NetworkSettings.IPAddress}}] net={{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}'ip=[] net=Expected resultA
localhost/-prefixed image, a short-name failure, and an empty IP field.Success conditionYou can name the four changes most likely to break an existing script.
-
Things that will surprise you in production
Quieter, and worse for it.
Source IPs are wrong. A containerised server sees
169.254.1.2for every external client (guide 55). Rate limiting, allowlists and access logs keyed on IP stop working, and nothing errors.--read-onlybreaks published ports. The container runs, serves its own loopback, and resets every forwarded connection (guide 81). Standard Docker hardening advice does not transfer.Files written by non-root container users are undeletable. A build container leaves you a tree owned by UID 100999 (guide 61). Use
--userns=keep-id.Nothing restarts at boot without lingering. A rootless service starts at first login, so a rebooted host serves nothing until someone connects - and it fixes itself the moment you look (guide 41). This is the one most likely to cause a real outage.
Logs are in journald. Retention is
journald.conf, not a Podman setting, and log shippers expecting JSON files find nothing (guide 83).sudo podmanis a different machine. Not a more privileged view of the same one - a separate store with none of your images (guide 14).bash Example session sudo -n podman info --format 'graphroot={{.Store.GraphRoot}} rootless={{.Host.Security.Rootless}}'graphroot=/var/lib/containers/storage rootless=falsesudo -n podman imagesREPOSITORY TAG IMAGE ID CREATED SIZEExpected resultRoot's separate, empty store.
Success conditionYou can name the failure most likely to cause an outage after a reboot.
-
A migration order that works
Not a big-bang conversion. In this order, each step independently useful:
1. Fully qualify every image reference. Containerfiles, Compose files, scripts, manifests. This removes an entire class of ambiguity and is worth doing even if you never migrate.
2. Install Podman alongside and run one thing. Nothing is at stake; a rootless install touches no Docker state.
3. Add
--userns=keep-idanywhere a container writes to a bind mount of yours - developer machines first.4. Create user-defined networks for anything where containers talk to each other, rather than relying on default-network behaviour.
5. Move long-running services to Quadlet (guide 40) rather than porting
docker runflags. This is where the real gain is: systemd restart handling, dependency ordering and boot integration, none of which Docker's--restartgives you.6.
loginctl enable-lingeron every host running a rootless service. One command, and skipping it is the outage above.7. Only then decide about the socket. If Compose is central to your workflow, enable it. If it was only ever a convenience, Quadlet or
podman kube playis better - and both avoid the teardown problem in guide 70.What you gain, honestly. No daemon to run as root and no socket equivalent to hand out (guide 2). Rootless by default, with a verifiable UID mapping. Pods, which are genuinely useful for sidecars and produce Kubernetes YAML you can play back (guide 22). systemd integration that is better than anything Docker offers. And signature enforcement in the pull path (guide 82).
What you give up. Swarm has no equivalent. The ecosystem assumes Docker, so you will read
dockerin every tutorial. Rootless networking is slower and its source addresses are wrong. And you will hit rough edges like the netns teardown error more often than a Docker user hits anything comparable.bash Example session systemctl --user is-active web.serviceactiveExpected resultAn active Quadlet service - the end state worth migrating toward.
Success conditionYou have an order to migrate in and a clear view of the trade.
Troubleshooting
A Compose file works under Docker and not under Podman.
Why: Usually unqualified image names, or a feature the compose tool in use does not implement.
Fix:Qualify the images first, then check which compose tool is running (guide 71).
A CI job passes locally and fails on the runner.
Why: The runner has no cached images, so short-name resolution takes a different path.
Fix:Fully qualify. This is the same root cause as most "works on my machine" reports on Podman.
A monitoring agent reports no containers.
Why: It is looking for the Docker socket.
Fix:Enable
podman.socketand point it there withDOCKER_HOST, or use an agent that reads Podman directly.Everything works until the host reboots.
Why: Lingering, almost always.
Fix:
sudo loginctl enable-linger $USER. Check it before declaring a migration done.