Docker's defaults are so invisible that the move begins by making them explicit. Separate what has to stay familiar - Dockerfile builds, registry login, published ports, volume mounts, and service names between containers - from what can change underneath. The hardest cases are developer laptops that rely on Docker Desktop's file sharing, and CI jobs and scripts that talk to the Docker socket directly. Those two often point to different replacements, so decide up front whether you are solving for local development, for production runtime, or for both.
For a developer machine, Podman is the closest fit: a daemonless CLI that mirrors Docker's commands, runs containers rootless, understands both OCI and Docker image formats, and brings up a managed Linux virtual machine on macOS or Windows through podman machine. It even exposes a Docker-compatible REST API for tools that expect the socket. For servers and clusters, containerd sits lower in the stack - it is a runtime daemon meant to be embedded rather than driven by hand, and its built-in CRI plugin is what lets Kubernetes run containers without Docker at all.
Most of your workflow survives the change: Dockerfiles, OCI images, registry tags, and environment-variable configuration carry over. The friction is at the edges. Audit everything that assumes the Docker socket or exact CLI flags, convert Compose files one service at a time while checking networks, health checks, and mounts, and export named volumes with application-aware backups rather than raw filesystem copies. In CI, replace the build and run steps separately so a failure points at the builder, the runtime, or the registry login, not the whole pipeline.