← Back to list

From Docker to Podman, the Clean Way

How I migrated without breaking my repos, my habits, or my future self

Raymon Epping in continuous_insights · 2025-12-23 21:17 · 3 claps · 10.1 min read
#podman #docker #podman-vs-docker #podman-desktop #podman-compose
Open on Medium ↗
Wiki topics: PSY · Psychology ☁️ · DevOps & Cloud 🚀 · Self Improvement

From Docker to Podman, the Clean Way

How I migrated without breaking my repos, my habits, or my future self

© Image generated with DALL·E 3 via ChatGPT by OpenAI for illustrative and editorial purposes only.

© Image generated with DALL·E 3 via ChatGPT by OpenAI for illustrative and editorial purposes only.

At some point, “it works on my machine” stops being funny. Not because the containers stopped running, but because the ecosystem around Docker started to feel heavier than it needed to be.

Licensing conversations. Desktop requirements. Background services. Surprise updates. And that subtle feeling that my local container workflow had become a product choice, not a dev choice.

So I decided to shift my local workflow from Docker to Podman.

Not with a scorched-earth migration. Not with “replace Docker with Podman everywhere and pray”.

I wanted a clean way:

  • Keep my Docker projects intact
  • Add a Podman lane next to them
  • Make the diff small
  • Make rollbacks boring

That is how **Podshift** was born.

What I actually wanted (and what I refused to do)

I wanted to run my existing Docker Compose projects with Podman, especially on macOS where Podman typically runs via a VM.

But I refused to:

  • Rewrite every repo manually
  • Rename files to “Podman-first” naming just to feel pure
  • Alias docker to podman and pretend nothing changed
  • Break teams who still use Docker

So I built a workflow that treats Podman as an additional lane, not a replacement hammer.

Why shift from Docker to Podman (my two honest reasons)

1) Control and simplicity

Podman feels more like a tool and less like a platform.

I want containers as plumbing, not a subscription conversation. I also want my local workflow to survive changes in Desktop tooling, licensing rules, or vendor strategy.

2) A better long-term dev posture

Even if your company still runs Docker everywhere, there is value in having a second lane:

  • It reduces ecosystem lock-in
  • It nudges you toward OCI and standards-based habits
  • It gives you a fallback when Docker Desktop is not available or not desirable

This is not “Docker is bad”. This is “I want optionality”.

Why I built Podshift in Node.js (the second “why”)

This one is simple: I wanted a CLI that is:

  • Fast to iterate on
  • Easy to distribute cross-platform
  • Friendly to parse JSON, YAML, file trees, and Compose structures
  • Straightforward to package

Node.js is great for this kind of filesystem-heavy, text-processing CLI. It is a glue language in the best possible sense, and it let me move quickly while keeping the code readable.

Also, I already live in the Node ecosystem with Nuxt and Express in other projects, so I leaned into what I can maintain.

Docker Compose vs podman-compose (and what I do with it)

Here is the key realization that shaped Podshift:

I do not need to delete docker-compose.yml. I just need a Podman-friendly derivative next to it.

So Podshift generates:

  • podman-compose.yml derived from your existing Compose file
  • Optional *.podman overlays for env_file entries when needed
  • A MIGRATION.md that explains what changed and why

The first transformation I cared about was the common macOS Podman friction point:

  • Docker land: host.docker.internal
  • Podman land: often host.containers.internal

So instead of telling people “change your Compose”, I generate a Podman version that swaps it.

Clean. Safe. Reversible.

Docker Desktop vs Podman Desktop (my take)

You can absolutely run Podman without Podman Desktop, just like you can run Docker without Docker Desktop.

But the difference is how it feels:

  • Docker Desktop is often the assumed default for Docker workflows
  • Podman Desktop is optional. The CLI-first experience is still solid

For me, Desktop is a convenience layer. I want my workflow to live in the repo and the terminal, not depend on a GUI being present or healthy.

Do I need to alias docker to podman?

No. And I generally do not.

Aliasing can hide real differences and create confusing bug reports like:

  • “docker-compose behaves weird”
  • “docker build failed”

…when you are actually calling Podman under the hood.

That said, you can alias if you know what you are doing and you control the environment. I treat that as a personal shortcut, not a team standard.

Podshift is explicitly designed so you do not need aliasing. It creates a parallel path instead.

Podshift workflow in real life

This is the mental model I follow.

1) Find candidates across your filesystem

When I have a big folder tree (like ~/Documents) I want one command that answers:

“What Compose projects exist here?”

So I run:

podshift candidates --root ~/Documents

It scans, groups, and reports Compose project roots. By default, it collapses children. That matters.

If a folder has a Compose file, I only need to know “this folder is a Docker project”. I do not want spam from backend/frontend subfolders unless I ask for it.

If I want deeper listing:

  • --include-child-projects includes nested Compose projects
  • --include-children lists Dockerfiles under each project (optional)

I also added output formats because sometimes you want terminal output, sometimes you want a report artifact:

podshift candidates --root ~/Documents --format json
podshift candidates --root ~/Documents --format md --out ./output/candidates.md

Optional fast hints are available too:

podshift candidates --root ~/Documents --hints

Those hints are intentionally bounded and quick. They detect things like:

  • host.docker.internal
  • /var/run/docker.sock
  • privileged or host namespace usage

It is not deep analysis. It is a fast “heads up”.

Ignore rules that actually help in large trees

Once you scan large directory trees, ignore rules become the difference between “useful tool” and “why did I do this”.

So I added .podshiftignore support.

What I ended up liking most is Option B:

  • Read ${rootAbs}/.podshiftignore
  • Also read ${HOME}/.podshiftignore if different
  • Merge both

Why? Because I have global noise patterns I always want ignored, plus repo-specific ignores when needed.

Also, I made the tool show what ignore file was actually used, because debugging ignore behavior without visibility is miserable.

Example output:

  • - Ignore: defaults=yes | ignoreFile=/Users/raymon.epping/.podshiftignore

I show the loaded one, and I hide missing ones. Missing is not interesting. Loaded is.

2) Migrate a repo cleanly

This is the “generate Podman lane” step:

podshift migrate --root . --compose ./docker-compose.yml

This generates:

  • podman-compose.yml
  • MIGRATION.md
  • Optional *.podman overlay env files
  • A small JSON artifact in your output folder so you can audit what was created

I keep overwrite behavior strict by default. If files exist, I want an explicit decision, not accidental clobbering.

3) Run via Podman without drama

Once I have podman-compose.yml, I can run:

podshift run --file ./podman-compose.yml --project-dir .

I like this because it centralizes the “how to run” path and keeps it consistent.

4) Clean Docker artifacts (archive, not delete)

After I validated the Podman path, I might want to clean Docker artifacts from the repo, but I still want a rollback.

So podshift clean can archive Docker artifacts into:

.podshift/archive/<timestamp>

Example:

podshift clean --root . --compose ./docker-compose.yml

Dry-run first:

podshift clean --root . --compose ./docker-compose.yml --dry-run

This step is optional, but powerful for repos where you want to commit to Podman and reduce clutter.

5) Restore if needed (boring rollback is the goal)

If I archived something and want it back:

podshift restore --root .

Dry-run is supported too:

podshift restore --root . --dry-run

This is the part I care about most: being able to experiment without fear.

Archives that are actually usable

I also added:

podshift archives --root .
podshift archives --root . --latest

Because nobody wants to copy-paste timestamps every time.

I also show file counts per archive using a fast bounded scan, because it is useful context without being expensive.

Report output modes, without breaking defaults

I like nice reports, but I do not want my CLI to suddenly become markdown-first and ruin the terminal experience.

So I kept the default as classic terminal summary, and I added:

  • --format text (default)
  • --format md
  • --format json

That gives me:

  • Human output for quick scanning
  • Markdown for writing or sharing
  • JSON for automation and future pipelines

A real migration: my podshift_example repo (end to end)

I always like tools more when I can point to one tiny repo and say: “this is the exact flow”.

So I created a minimal example folder called podshift_example that starts as a plain Docker Compose project and ends as a Podman-friendly repo with clean artifacts.

Step 1: create a tiny NGINX site with Docker Compose

I created a simple structure:

  • Dockerfile based on nginx:1.27-alpine
  • nginx/default.conf for server config
  • site/index.html for a hello page
  • docker-compose.yml to build and run it on port 8080

Then I built and ran it:

docker compose up -d --build
curl -sSf http://localhost:8080/ | head
curl -sSf http://localhost:8080/healthz
docker compose down

And it worked exactly like I want a demo to work: no magic, no excuses.

The browser output was:

  • It works ✅
  • Docker Compose → Podman Compose → Nomad (more on Nomad later, and why I stopped)

Step 2: run Podshift and generate the Podman lane

Then I ran Podshift migration in that repo.

The important part is what ended up in the working tree afterwards.

This was my folder tree after the migration:

./
├── .podshift/
│   └── archive/
│     └── 2025-12-23T16-33-29-249Z/
│       ├── .dockerignore
│       └── docker-compose.yml
├── nginx/
│   └── default.conf
├── output/
│   └── podshift/
│     ├── migrate.json
│     ├── recommendation.json
│     ├── report.json
│     └── report.md
├── site/
│   └── index.html
├── FOLDER_TREE.md
├── LICENSE
├── MIGRATION.md
├── podman-compose.yml
├── README.md
└── sanity_check.md

That tree is the entire point of Podshift:

  • My original Docker artifacts were not destroyed
  • The Podman lane exists as explicit files in the repo
  • A migration doc exists, so future-me does not need to remember why
  • The output folder contains JSON and reports, so automation can hook in later
  • The archive exists so rollback is not a vibe, it is a command

When I saw that tree, I literally thought: “yep, this is boring in the best way”.

Step 3: candidate scanning and ignore visibility (the quality-of-life part)

Before and during this work, I used podshift candidates heavily to scan larger trees.

I wanted two things:

  1. Candidate discovery across huge folder trees
  2. Visibility into the ignore file actually used

So I added output like this:

  • - Ignore: defaults=yes | ignoreFile=/Users/raymon.epping/.podshiftignore

Loaded is useful. Missing is noise. So Podshift only shows the ignore files that are actually loaded.

That sounds minor, but when you scan 20k entries and get different results, it matters.

Why I intentionally stopped at Podman (and left Nomad out)

I originally wanted the “perfect ending”:

Docker Compose → Podman Compose → Nomad

And I did try it.

But the reality is: getting Nomad to orchestrate Podman on macOS in a clean, repeatable way is not a quick “wrap it up” conclusion. It turns into a separate technical deep dive, involving runtime drivers, socket API exposure, and what you do or do not want your local machine to be responsible for.

In my case, the moment I felt the friction and complexity rising, I made the call:

This article is about a clean local transition from Docker to Podman, without breaking repos.

So I left Nomad out of this piece, on purpose.

Nomad deserves its own article, with the right focus and the right constraints, instead of being a rushed victory lap at the end.

Should Podshift format YAML with Prettier?

I thought about this too.

Right now, Podshift writes podman-compose.yml through the YAML writer you already use. That is usually “good enough”, but it can produce YAML that is technically correct yet not the style you want.

So here is what I would do next, without making the tool fragile:

  • Optional formatting step, not default
  • Use Prettier only if available, or ship it as a dependency if you accept the weight

Something like:

  • podshift migrate --prettier

or podshift fmt --file podman-compose.yml

I lean toward “optional and explicit”. Formatting is subjective. Correctness is not.

Next projects: Dockerfile, Containerfile, or both?

This is where I keep it pragmatic.

What I do by default

I keep writing Dockerfile as the primary build file.

Reason: the tooling world still assumes it. CI pipelines, scanners, docs, and developer muscle memory.

Podman builds Dockerfiles just fine.

When I add a Containerfile

I add a Containerfile when I want to make the repo Podman-first in intent, without breaking anything.

Two sane patterns:

Pattern A: identical copy

  • Dockerfile
  • Containerfile (same content)

Pattern B: keep one file, build explicitly

  • only Dockerfile
  • build with podman build -f Dockerfile .

I do not try to get clever with “includes”. Dockerfile format does not support that cleanly.

What Podshift should do here

Podshift should stay conservative:

  • Do not rename Dockerfile
  • Do not force Containerfile
  • Only generate Podman compose artifacts and overlays

Then later, add an opt-in:

  • podshift migrate --emit-containerfile

That would generate Containerfile if a Dockerfile exists, without deleting or renaming anything.

Wrapping it: Homebrew install, because this is a CLI

At some point I looked at this and thought:

“Ray. Why don’t you wrap it?”

So I did.

Packaging Podshift as a Homebrew formula makes it install like a real tool, not a repo you have to keep cloning.

That forced good discipline:

  • predictable installation
  • versioned releases
  • repeatable upgrades
  • stable CLI behavior

It made the project feel mature fast.

And yes, once it installs cleanly via Homebrew, it changes how the tool feels. It stops being “my repo project” and starts being “a thing I can actually use”.

Install it yourself

If you want to try this exactly the way I’m running it, you have two paths: install the CLI via Homebrew, or run it from source.

Option A: Homebrew (recommended)

This is the fastest way to get a working podshift binary on your machine.

brew install raymonepping/podshift/podshift
podshift --help

Option B: Run from source (GitHub)

If you prefer to inspect the code, tweak behavior, or just keep it local:

Repo: https://github.com/raymonepping/podshift

Typical flow:

git clone https://github.com/raymonepping/podshift
cd podshift
npm install
node ./bin/podshift --help

If you want a global-ish local install for testing:

npm link
podshift --help

The demo repo I used in this article

I also published the tiny NGINX example repo I used for the Docker Compose → Podman Compose transition:

Demo repo: https://github.com/raymonepping/podshift_example

So if you want to reproduce the exact flow:

git clone https://github.com/raymonepping/podshift_example
cd podshift_example

docker compose up -d --build
curl -sSf http://localhost:8080/ | head
docker compose down

podshift migrate --root . --compose ./docker-compose.yml
podshift run --file ./podman-compose.yml --project-dir .

The outcome

Podshift did not “replace Docker”.

It gave me a clean migration lane:

  • minimal repo churn
  • explicit Podman artifacts
  • reversible cleanup
  • scalable discovery across big folder trees
  • and a CLI I can ship

That is the clean way.

What’s next (when I feel like it)

The tool is already useful, but future upgrades I would consider:

  • Optional YAML formatting for generated Podman compose
  • A dedicated fmt command
  • More fast hints, but still bounded
  • A “project profile” report that summarizes risk and effort in one page

But for now?

This is mature enough. It does the job. It is safe. It is boring in the best way.

And I can finally move forward without dragging Docker Desktop decisions into every repo.

🧠 Born from **How I Use AI as My DevOps Copilot 🧬 Wired for [Curiosity](https://medium.com/p/5a1f1d2dc002) 🤖 Powered by [Sally](https://openai.com/chatgpt) — my AI DevOps copilot 🚀 Because [automation](https://medium.com/continuous-insights/the-daily-joy-of-automation-a-personal-note-7df8f83881bf)** should automate itself

📘 Journey recap : **Turning From 50 Shades of Red into Blue**


메타데이터
post_id
e57f3be45d34
slug
from-docker-to-podman-the-clean-way-e57f3be45d34
url
https://medium.com/continuous-insights/from-docker-to-podman-the-clean-way-e57f3be45d34
canonical_url
https://medium.com/continuous-insights/from-docker-to-podman-the-clean-way-e57f3be45d34
author_url
https://medium.com/@raymonepping
status
ok
fetched_at
2026-06-24 11:06:28