Podman for People Who Distrust the Docker Daemon
Daemonless, rootless, and answerable to systemd

Contents
I ran Docker happily for years, and then one afternoon I actually read what my
docker group membership meant and felt slightly sick. I had added my everyday
user to that group ages ago because typing sudo in front of every command is
tedious, the way everyone does, the way every tutorial tells you to. What nobody
mentions in the same breath is that this is functionally the same as handing that
user a password-free root shell. Podman is the container engine I moved to when
that stopped sitting right with me, and I have not wanted to go back.
Let me explain the why before any of the how, because the why is the entire reason I bothered to relearn muscle memory I had spent a decade building.
The daemon is the problem, and it’s a bigger one than it looks
dockerd is a long-running daemon that runs as root. Every container you launch
is a child of that one process. It listens on a Unix socket, /var/run/docker.sock,
and anything that can talk to that socket can ask the daemon to do root things on
its behalf. That is the whole design, and it works, but it has consequences that
are easy to wave away until you sit and think about them.
Membership of the docker group grants access to that socket. With access to
that socket I can run a container that bind-mounts the host’s / into itself and
then chroots into it. At that point I am editing /etc/shadow on the host as
root, from an account that was supposed to be unprivileged. There is no
escalation trick here, no CVE, nothing clever — this is the documented, intended
behaviour of the tool. The docker group is root with extra steps, and the extra
steps take about four seconds.
Then there is the blast radius. One process is the parent of every container on
the box. If dockerd wedges, gets OOM-killed, or you restart it during an
upgrade, everything it supervises is affected at once. I have watched a daemon
restart take down a stack of unrelated services that had no business sharing a
fate, purely because they shared a parent. For a homelab where I am running a
dozen small things that should be independent, that single point of failure
always nagged at me.
None of this makes Docker bad software. It makes Docker a piece of software with an architecture I would rather not build my defence-in-depth on top of. The root daemon is load-bearing, and I wanted it out from under the floor.
How Podman does it differently
Podman throws the daemon away. There is no long-running privileged process
listening for instructions. When I type podman run, the Podman binary itself
does the work of setting up the container, then fork-execs a small supervisor
called conmon (container monitor) that stays attached to that one container to
handle its I/O and record its exit status. Each container gets its own conmon.
The containers are children of my shell session, or of systemd if I wire them up
properly, and they answer to me.
The immediate consequence is that there is nothing privileged sitting idle by default. No socket that is root-equivalent, no group whose members are secretly administrators. If a Podman process dies, it takes its own container with it and leaves the others entirely alone. The failure domain shrank to exactly one container, which is where I wanted it.
The second thing Podman does is run rootless as the default. My ordinary user owns its containers. Inside the container, processes still think they are running as root — UID 0, the way container images expect — but that UID 0 is mapped through a Linux user namespace to my unprivileged host UID. Root in the container is a lie the kernel tells the container, and on the host it is just me. If something breaks out, it breaks out into an account that can barely do anything.
That mapping is driven by two files, /etc/subuid and /etc/subgid, which hand
each user a range of subordinate IDs, typically 65,536 of them. So my host UID
might be 1000, and the container’s UID 0 maps to 1000, while UID 1 inside maps to
the first entry of my subordinate range, say 100000, and so on up the block. If
those files have no entry for your user, rootless Podman cannot build the mapping
and refuses to start containers, which is the first thing I check when a fresh
box misbehaves. I go deeper on user namespaces and why this matters for least
privilege in Rootless containers: least privilege for the paranoid;
here it is enough to know the mapping exists and where it comes from.
The part that made the switch painless: the CLI is a near drop-in for Docker’s.
The verbs and flags are the same, it pulls OCI images from the same registries,
and it reads the same Dockerfiles. Most days I run alias docker=podman and
carry on as though nothing changed. My old Compose files still work through
podman compose, and the Compose habits I wrote up in
Docker Compose patterns that age well
carried across intact — the same restart policies, named volumes, and healthcheck
conventions behave the way I expect.
Podman also leans into the Kubernetes model in a way Docker never did. It groups
containers into a pod — the actual Kubernetes concept — where the members share
a network namespace and can reach each other on localhost. Better still,
podman kube generate emits Kubernetes YAML from a running pod, and
podman kube play builds a pod back from that YAML. When a homelab service
eventually earns a place on a real cluster, that round-trip is a genuine bridge
rather than a rewrite.
Quadlet: handing your containers to systemd properly
For a long time the answer to “how do I keep this container running across
reboots” was podman generate systemd, which spat out a unit file you copied
into place. That approach is deprecated now, and good riddance — the generated
units were verbose and awkward to maintain. The modern way is Quadlet.
Quadlet lets me describe a container in a small declarative file that looks like
a systemd unit, drop it in the right directory, and let a generator translate it
into a real .service at boot. For rootless containers the directory is
~/.config/containers/systemd/; for system-wide ones it is
/etc/containers/systemd/. Here is the unit I use for a small self-hosted web
app, saved as ~/.config/containers/systemd/linkwarden.container:
| |
After writing that I run systemctl --user daemon-reload so the generator picks
it up, then systemctl --user start linkwarden. Podman has manufactured a proper
service called linkwarden.service behind the scenes, and I manage it with all
the ordinary systemd verbs — status, restart, journalctl --user -u linkwarden
for logs. The [Install] section with WantedBy=default.target means it starts
automatically for my user session. A first rootless run and enabling boot-time
start looks like this:
| |
That AutoUpdate=registry label is worth calling out. It marks the container as
eligible for podman auto-update, which checks the registry for a newer digest
on the tag you pinned, pulls it, and restarts the service cleanly if the image
moved. Podman ships a systemd timer, podman-auto-update.timer, that runs this on
a schedule, so my homelab keeps itself current without me babysitting it. I keep
it on for leaf services and off for anything where I want to read the changelog
first.
One more piece worth naming is rootless networking, because it works differently
from the root case. Recent Podman uses pasta by default to give each rootless
container a userspace network stack, with slirp4netns as the older fallback.
For containers that need to find each other by name on a shared network, Podman
runs aardvark-dns, a tiny DNS server that resolves container names within that
network. This is why the Network=web.network line above lets my proxy reach the
app by hostname rather than by juggling IP addresses.
Troubleshooting the things that will actually bite you
The switch is smooth right up until it isn’t, and the rough edges are all consequences of running rootless. Here are the ones that cost me time so they cost you less.
You cannot bind ports below 1024. Rootless means unprivileged, and
unprivileged processes are not allowed to bind low ports. So PublishPort=80:80
fails outright. My default answer is to publish a high port like 8100 and put a
reverse proxy in front, which is how the Quadlet above is written. If you genuinely
need a container on a low port, lower the kernel’s threshold with
sysctl net.ipv4.ip_unprivileged_port_start=80 and make it persistent in
/etc/sysctl.d/.
The service dies when you log out. By default a user’s systemd session and
everything under it is torn down when the last session for that user ends. Log out
of SSH and your carefully configured container evaporates. The fix is
loginctl enable-linger smarc, which tells systemd to keep a user manager running
for that account whether or not anyone is logged in. Forgetting this is the single
most common reason people conclude rootless Podman “doesn’t stay up”.
Memory and CPU limits silently do nothing. Flags like --memory and
--cpus need cgroups v2 with delegation to the user, so the user manager is
actually permitted to set those controllers. Modern distributions ship cgroups v2
and delegate the right controllers, but on an older or hand-configured box you can
set a limit, see no error, and get no enforcement. Check podman info for the
cgroup version and confirm the memory and cpu controllers appear in your
user’s cgroup.controllers.
File ownership on volumes looks wrong. Because of the UID shift, a file the
container writes as its UID 0 lands on the host owned by your subordinate range,
and a file you created on the host as UID 1000 shows up inside the container owned
by some high, unfamiliar number. The clean fix is the :U mount option, as on the
Volume line above, which tells Podman to chown the volume’s contents to match
the container’s mapped user on startup. For an existing directory you can do the
remap by hand with podman unshare chown -R 0:0 /path/to/data, which runs the
chown inside the same user namespace the container will see.
Ping doesn’t work from inside a rootless container. ICMP from an unprivileged
user namespace needs the kernel to allow it, controlled by
net.ipv4.ping_group_range. If ping fails but everything else networks fine, that
sysctl is why; widen the range in /etc/sysctl.d/ and it starts working. It trips
people up during debugging because they assume the network itself is broken.
Your home partition fills up. Rootless storage lives under
~/.local/share/containers, not under /var/lib where the root daemon kept
everything. Images, layers, and volumes all accumulate there, and on a box where
/home is a modest partition, a few large images will fill it quietly. I keep an
eye on it with podman system df and run podman system prune on a schedule.
Rootless refuses to start at all. If Podman complains about being unable to
set up the user namespace, check that your user has entries in /etc/subuid and
/etc/subgid. A user created before the subordinate-ID tooling was configured,
or added by a script that skipped it, will have no range allocated. Running
usermod --add-subuids 100000-165535 --add-subgids 100000-165535 smarc and then
podman system migrate sorts it out.
The honest verdict
Podman is not free of friction. Rootless networking has more moving parts than the root daemon’s tidy bridge, the low-port restriction will annoy you at least once, and the UID-shifting on volumes is a genuine conceptual hurdle the first few times you meet it. If you run a single-purpose server where a root daemon is the entire threat model already, and you have no plans to change that, Docker will keep working fine and I would not evangelise at you.
For my homelab the trade came out firmly in Podman’s favour. I removed a root
daemon and a root-equivalent group from my attack surface, I gave every container
its own failure domain, and I got systemd management through Quadlet that is
cleaner than anything I hand-rolled under Docker. The auto-update timer keeps my
leaf services current, and podman kube play means the work I do now points at a
real cluster later. The relearning cost me an afternoon; the peace of mind has
lasted a lot longer than that. If the root daemon has ever given you the same
uneasy feeling it gave me, spend the afternoon.



