CertGrid CertGrid
Best Practices·Podman

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

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.

Every command on this page ran on podman01.
Server NameIP AddressOSRolesCPURAMHDD
PODMAN01192.168.0.24Ubuntu 26.04 LTSPrimary Container Host2 Core4 GB50 GB

Before you start

  1. 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, --format templates, -v, -p, -e, --restart, Dockerfiles unchanged, and docker itself via the podman-docker shim (guide 4). A Makefile full of docker commands 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       NAMES

    Expected resultpodman version 5.7.0 from a docker command, and an empty container list.

    Success conditionYou have Docker commands running under Podman.

  2. 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:1 produces localhost/app:1. Anything grepping podman images for ^app finds 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-world resolves 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.IPAddress is 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:80 needs a sysctl or a proxy (guide 51).

    6. docker info fields do not exist. No ServerVersion, because there is no server. Anything parsing info output 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.

  3. Things that will surprise you in production

    Quieter, and worse for it.

    Source IPs are wrong. A containerised server sees 169.254.1.2 for every external client (guide 55). Rate limiting, allowlists and access logs keyed on IP stop working, and nothing errors.

    --read-only breaks 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 podman is 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     SIZE

    Expected resultRoot's separate, empty store.

    Success conditionYou can name the failure most likely to cause an outage after a reboot.

  4. 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-id anywhere 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 run flags. This is where the real gain is: systemd restart handling, dependency ordering and boot integration, none of which Docker's --restart gives you.

    6. loginctl enable-linger on 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 play is 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 docker in 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.serviceactive

    Expected 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

Official sources