From Docker to Podman, the Clean Way
How I migrated without breaking my repos, my habits, or my future self
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.
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
dockertopodmanand 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.ymlderived from your existing Compose file- Optional
*.podmanoverlays forenv_fileentries when needed - A
MIGRATION.mdthat 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-projectsincludes nested Compose projects--include-childrenlists 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}/.podshiftignoreif 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.ymlMIGRATION.md- Optional
*.podmanoverlay 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:
Dockerfilebased onnginx:1.27-alpinenginx/default.conffor server configsite/index.htmlfor a hello pagedocker-compose.ymlto 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:
- Candidate discovery across huge folder trees
- 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
DockerfileContainerfile(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
fmtcommand - 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