Skip to content

Why I Switched to Podman (and Why You Might Too)

I’ve been a Docker loyalist for years. But lately, I’ve been experimenting with Podman, and honestly? It’s grown on me.

The switch started out of necessity. I’ve been working on FEGA for a while, I needed rootless containers for security reasons. Docker can do rootless, but it always felt like an afterthought. Podman was built for it from day one.

What’s the Difference?

At the surface, not much. Podman is CLI-compatible with Docker. This works:

alias docker=podman

Seriously. Most commands just work. But architecturally they’re different.

Docker runs a daemon. Every container talks to dockerd, which runs as root. Podman doesn’t have a daemon. Containers run as child processes of your shell. No daemon means no single point of failure, no root process sitting there waiting.

The Gotchas

A few things bit me:

  1. Compose — There’s podman-compose and podman kube play, but honestly? We still use docker-compose with Podman as the backend. It just works. Set DOCKER_HOST to your Podman socket and your existing compose files run unchanged, or let podman compose do that wiring for you, since it hands off to docker-compose when it’s installed. Sometimes the boring solution is the right one.
  2. Networking — Containers on the default network can’t find each other by name. Docker’s default bridge has the same limit; it only feels like it works because compose quietly creates a network per project. Outside compose, in either tool, you make your own:
podman network create mynet
podman run --network mynet --name app1 myimage
podman run --network mynet --name app2 myimage
  1. Build caching — This is where Docker still wins. BuildKit has SPOILED ME.

Docker BuildKit

It builds independent stages in parallel. If our multi-stage Dockerfile has a frontend and backend that don’t depend on each other, they build at the same time. It also does content-addressed caching! Meaning that a COPY only misses the cache when the files it copies actually changed, not just because their timestamps did.

For example:

FROM node:24 AS frontend
# build frontend (base image #1) ...

FROM golang:1.27 AS backend
# build backend (base image #2)...

FROM nginx:alpine
# build final stage (base image #3)...
COPY --from=frontend /ui/dist /usr/share/nginx/html
COPY --from=backend /api/app /app
The three stages of the Dockerfile above drawn against a time axis, three ways. BuildKit by default, with docker build: frontend on node:24 and backend on golang:1.27 run at the same time, and the final nginx:alpine stage starts once both are done. Buildah by default, with podman build: frontend, then backend, then final, one after another, finishing well past the other two rows. Buildah with podman build --jobs=0: the same overlap as BuildKit, finishing at the same point. The caption reads: same three stages, Buildah only overlaps them when you pass --jobs.

Podman uses Buildah, which builds stages one at a time unless you pass --jobs. podman build --jobs=0 lets independent stages run in parallel, but BuildKit does that by default and its caching is still ahead. For simple images you won’t notice. For anything with heavy multi-stage builds, you will.

When to Use What

I still reach for Docker when I need fast builds, or I’m just spinning up something throwaway. For anything security-sensitive or closer to production? Podman.

They read the same Dockerfiles, pull from the same registries, produce OCI-compliant images. Switching between them costs nothing.


Been away from writing for most of 2025. Feels good to be back.

Thanks for reading.

Follow the RSS feed · Say hi on LinkedIn

Human Agent