Podman can build Docker images, but it uses a slightly different internal mechanism that can trip you up if you expect it to behave identically.

Let’s see what that looks like in practice.

Imagine you have a simple Dockerfile:

FROM alpine:latest
COPY . /app
RUN echo "Hello from my container!" > /app/message.txt
CMD ["cat", "/app/message.txt"]

And a file named message.txt in the same directory.

To build this with Podman, you’d run:

podman build -t my-alpine-app .

This command executes the instructions in the Dockerfile. Podman creates a series of intermediate containers (one for each RUN or COPY instruction) and commits their filesystem changes as layers to form the final image. The FROM instruction pulls the base image (alpine:latest in this case) if it’s not already present locally. The COPY instruction transfers files from your build context (the current directory .) into the image’s filesystem. The RUN instruction executes a command within the container’s environment. Finally, the CMD instruction sets the default command to run when a container is started from this image.

Here’s the mental model:

  1. Base Image: Podman pulls the specified base image. This is the starting point, like an empty canvas.
  2. Layered Filesystem: Each instruction in the Dockerfile that modifies the filesystem creates a new read-only layer on top of the previous one. This is highly efficient for storage and sharing layers between images.
  3. Build Context: The directory you specify (.) is sent to the Podman daemon (or the Podman client if running in rootless mode, which is common). This context contains your Dockerfile and any files you intend to COPY or ADD.
  4. Execution Environment: Each RUN instruction executes commands in a temporary container based on the current state of the image layers.
  5. Final Image: The result is a new image with all the layers stacked, ready to be run as a container.

You can verify the build by running a container from your new image:

podman run --rm my-alpine-app

This should output:

Hello from my container!

The key difference from Docker, and where compatibility issues often arise, is in how Podman handles the build process, especially concerning networking and user permissions when running rootless. Docker, by default, runs the build daemon as root, giving it broad access. Podman, particularly in its rootless mode, operates as a regular user, which means it emulates certain root privileges within a user namespace. This can affect how network-bound operations or file permission changes within the build context behave.

For example, if your Dockerfile tried to download a package using apt-get or apk and relied on the build environment having full root access to manipulate system files in specific ways, you might encounter permission errors when running Podman rootless. The solution often involves ensuring file permissions are correct before the build, or using specific Podman flags to adjust user namespace mappings, though this is less common for standard image builds.

One thing that often catches people is the implicit handling of the ADD instruction. While COPY is straightforward file copying, ADD can also extract tar archives from remote URLs or local paths. If you’re using ADD with a local tar file, Podman will automatically extract it, just like Docker. However, if you use ADD with a URL, Podman fetches the content and places it in the image, but it doesn’t automatically handle extraction of compressed files (like .tar.gz) unless explicitly instructed to do so via a RUN command. This subtle difference can lead to unexpected behavior if you’re migrating complex Dockerfiles that rely on ADD’s archive extraction magic.

The next common hurdle you’ll face is understanding how Podman handles multi-stage builds and the sharing of build artifacts between stages, particularly when dealing with complex dependencies or build tools.

Want structured learning?

Take the full Podman course →