How do you migrate from Docker to Podman in CI/CD?
Quick Answer
Most Docker CLI commands work unchanged against Podman because Podman implements Docker's CLI surface, so aliasing docker to podman covers the majority of build and run steps in a CI pipeline. The real migration work is everywhere Docker Compose or tooling expects a live Docker socket — those need podman-compose, Podman's Docker-API-compatible socket service, or a rewrite to Podman-native pod definitions, and CI runner images need a properly configured subuid/subgid range for rootless execution to work at all.
Detailed Answer
Migrating from Docker to Podman in CI is a bit like switching a delivery fleet from gas trucks to electric trucks that use the exact same steering wheel and pedals — most drivers, the build scripts, won't notice the difference and can keep driving the same way, but the depot's gas pumps, the Docker daemon and its socket, are gone, so anything that specifically depended on stopping by the gas pump, rather than just driving the truck, needs a new plan.
Podman was deliberately built to be a drop-in CLI-compatible replacement for Docker specifically to make this kind of migration low-friction; podman build, podman push, and podman run accept the same flags and Dockerfile syntax as their Docker equivalents. This matters in CI/CD because most pipeline YAML is just shell commands invoking the docker binary, so an alias docker=podman, or symlinking the binary, silently redirects the vast majority of a pipeline's build and push steps with zero changes.
The migration breaks down into three real steps. First, alias or symlink docker to podman in the CI runner image so existing docker build and docker push invocations work unmodified. Second, handle Docker Compose: docker-compose itself expects a Docker daemon API to talk to, so teams either install podman-compose, a Python wrapper translating compose YAML into sequential Podman commands, or start Podman's Docker-API-compatible socket service via podman system service and point DOCKER_HOST at it so the original docker-compose binary keeps working unmodified. Third, and most often missed, is ensuring the CI runner's user has a valid /etc/subuid and /etc/subgid allocation, because rootless Podman refuses to create the user namespace mapping without it, and container-based CI runners frequently run as a freshly created user with no such allocation configured in the base image.
In production CI fleets, the practical issues that surface are storage driver differences, since some runner images bake in overlay2 assumptions that don't hold for Podman's fuse-overlayfs default under rootless mode, causing slower image layer extraction, CI plugins that specifically shell out to docker binary paths or check docker version output format for parsing, and multi-stage build caching behaving slightly differently since Podman's build cache invalidation logic isn't byte-for-byte identical to Docker's BuildKit. Teams should run a subset of pipelines in shadow or parallel mode against both engines for a few weeks before fully cutting over, watching build duration and cache hit rate as the key regression signals.
The gotcha that catches teams off guard is that some third-party Docker plugins and vulnerability scanners integrate via the Docker socket protocol specifically, not just the CLI, and silently produce empty or wrong results against Podman's compatibility socket if the socket path or API version isn't set exactly right — the scan appears to run successfully but silently returns zero findings, which is far more dangerous than an outright failure because nobody notices the security gate stopped actually checking anything.