Ditching the Whale: Running CI on Podman Instead of Docker

The runner sat on a box in the corner of the network, quietly polling a Forgejo server for jobs, same as it had for months. Nobody thinks about the daemon until the daemon is gone. Docker had been there since the beginning — the whale logo, the systemd unit, the socket nobody questioned. Then one day the whale swam off, and what was left was a runner with a workflow full of docker/build-push-action calls and nothing underneath them.

The corporate path is short: reinstall Docker, move on, never ask why a background daemon needed root just to build a tarball. The street finds its own uses for things, and the thing this time was Podman — daemonless, rootless, already sitting on the box doing quiet work for months without anyone noticing it was capable of more.

The first attempt was naive: install podman-docker, the little shim that aliases docker straight to podman, and assume the problem was solved. Basic commands worked immediately — run, build, push, cp, all speaking the same dialect. But the moment a workflow called docker buildx create --use, it broke. Podman’s Buildx compatibility is a shallow well: it answers build and version, but knows nothing of builder instances, contexts, or inspect --bootstrap. The official Buildx GitHub Actions expect a real daemon with BuildKit wired in underneath. Podman isn’t that, and pretending otherwise wastes an evening.

The fix wasn’t to fake Buildx. It was to stop needing it.

[Service]
Environment=DOCKER_HOST=unix://%t/podman/podman.sock
ExecStart=/usr/local/bin/act_runner daemon

Enable the podman socket, point DOCKER_HOST at it, and drop back to plain docker login / docker build -t ... -t ... / docker push in the workflow itself. No builder abstraction, no BuildKit, no daemon running as root in the background — just a rootless engine answering the same API surface the tooling already expected. Every tag that used to come from a metadata action became one extra -t flag. Every label became --label. Less indirection, not more.

Verification wasn’t a shrug and a hope. Tag a real release, watch the run go green on the server, then pull the pushed image back down by digest and diff it against what should have landed. When the SHA matches and the timestamp lines up with the run window, the pipeline isn’t theoretically fixed — it’s provably fixed.

Docker isn’t evil. But it’s not load-bearing either, and a home lab is exactly the place to find out what still stands once you kick a leg out. Turns out: all of it, running lighter than before.