Podman Full Tutorial: Linux and Termux
TL;DR: Podman is the daemonless, rootless-first container engine that runs OCI images (Docker Hub included) without a background daemon or a root-owned socket. This guide covers install on Ubuntu/Debian, Fedora, Arch and openSUSE, the rootless prerequisites, the Docker-compatible command set, systemd integration via Quadlet, and the honest Termux story — where the realistic path is proot-distro or remote control of a host, not a native package. You'll need a Linux box (or a phone with Termux) and about 20 minutes.
I typed docker run for years without thinking about the daemon. Then a lab box died at 3 AM, and the post-mortem was the same story you've heard a hundred times: the daemon had wedged itself on a stale iptables rule, and every container on the box went down with it. That single failure is why I moved my labs to Podman — and why this tutorial exists.
Podman isn't "Docker without the brand." It's a different architecture with the same muscle memory: same OCI images, same CLI, but no daemon, no /var/run/docker.sock, and a rootless security model that Docker only bolted on later. Here's the full walkthrough — desktop Linux first, then the messy, interesting Termux part.
What Podman Is (and Why It's Not Docker)
Docker is a client/server system: a root-owned daemon holds the API socket, spawns containers, and restarts them when the daemon crashes. Podman is a fork/exec model — every podman command talks directly to the OCI runtime (crun is the default, runc the former default) through conmon, a per-container monitor. No daemon, no socket, no single process that owns everything. Kill the CLI and your containers keep running; reboot the host and systemd (not a daemon) brings them back.
That architecture buys you four things that matter in a security lab:
- Rootless-first. Unprivileged users run containers via user namespaces and cgroups v2. No root daemon means no root-owned attack surface listening on a socket.
- systemd-native. Containers become systemd units (
systemd-run,podman generate systemd, and the modern Quadlet generator). This is Podman's superpower — Docker has no equivalent. - Kubernetes-shaped.
podman play kubeandpodman generate kubeconsume and produce pod manifests. Pods are a first-class concept, not an afterthought. - Drop-in fluency.
alias docker=podmancovers 95% of daily work, and thepodman-dockerpackage ships a realdockerCLI shim plus the Docker API socket if legacy tooling insists.
One honest caveat before we start: Podman's rootless mode leans on unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED). That kernel feature has a real security trade-off on multi-user hosts — check your distro's hardening stance (Debian and Ubuntu enable it by default; some hardened kernels disable it).
Installing Podman on Desktop Linux
Every major distro ships Podman in its official repos. No third-party repo needed on anything modern.
Debian and Ubuntu
Podman is in Debian 11+ and Ubuntu 20.10+ main repos. On Ubuntu 22.04 you get 3.4, on 24.04 you get 4.9 — fine for everything below. If you're on an older release, check backports or the upstream static builds rather than fighting a stale 3.x.
sudo apt update
sudo apt install -y podman fuse-overlayfs slirp4netns uidmap
podman --version
uidmap provides newuidmap/newgidmap, which rootless containers need to map subordinate IDs. fuse-overlayfs is the userspace overlay fallback for older kernels; slirp4netns is the classic user-space network stack (more on both below). Kali, being Debian-based, takes the same apt path.
Fedora and RHEL
Fedora and RHEL 8+ ship Podman as the default container engine — it's the tool the distro itself uses. One command, and the distro pulls the matching netavark, aardvark-dns and passt stack automatically.
sudo dnf install -y podman
# Optional: docker CLI compatibility shim + API socket
sudo dnf install -y podman-docker
podman --version
Arch and openSUSE
Arch carries Podman in extra (6.x as of this writing). One gotcha: Arch doesn't configure unqualified image registries by default, so podman pull alpine fails with "short-name did not resolve" until you add Docker Hub to registries.conf. openSUSE ships Podman built-in — nothing to install.
sudo pacman -S --needed podman podman-docker netavark aardvark-dns passt
# Arch: make short names resolve like Docker does
echo 'unqualified-search-registries = ["docker.io"]' | \
sudo tee /etc/containers/registries.conf.d/10-unqualified-search-registries.conf
# openSUSE (already installed): sudo zypper in podman
Rootless Prerequisites: What Docker Never Asked You For
Because Docker runs a root daemon, it never needed you to configure anything. Rootless Podman needs three quiet pieces of infrastructure. Miss one and you get the classic "operation not permitted" wall.
1. Subordinate UID/GID ranges (subuid/subgid)
Rootless Podman maps your unprivileged UID to UID 0 inside the container, and the container's other UIDs come from a reserved range in /etc/subuid and /etc/subgid. Users created on modern distros usually have entries already — check before you fix what isn't broken:
grep "$(whoami)" /etc/subuid /etc/subgid
# Expected: yourname:100000:65536 (start:count)
# If empty, allocate a range (as root):
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"
# Propagate to running rootless containers:
podman system migrate
Allocate at least 65536 IDs — many base images (alpine, busybox) need the full range. And yes, podman system migrate after editing subuid/subgid is a real step: rootless Podman keeps a "pause" process holding the namespace alive, and without the migrate it won't see your new ranges.
2. User-space networking: pasta vs slirp4netns
Rootless containers can't create real network namespaces, so Podman uses a user-space network stack. Since Podman 5, pasta (from the passt package) is the default; slirp4netns was the default up to 5. Both work; pasta is faster and copies your host's IP instead of NAT-ing. If you hit networking bugs, you can force the old stack in ~/.config/containers/containers.conf with default_rootless_network_cmd = "slirp4netns".
3. Overlay storage
Rootless overlay mounts used to require fuse-overlayfs; modern kernels and Podman support native rootless overlay, which is faster. Either way, your images live in ~/.local/share/containers/storage (the graphroot), with transient data in $XDG_RUNTIME_DIR/containers. Rootful Podman uses /var/lib/containers/storage.
And one thing you do not need: podman machine. That's the macOS/Windows story — a Linux VM under the hood. On Linux you run containers natively; skip the VM entirely.
Podman Command Walkthrough: Docker Muscle Memory
If you know Docker, you know Podman. The flags are identical by design. First run, then the daily loop:
# Smoke test — no sudo anywhere
podman run --rm quay.io/podman/hello
# Interactive shell, detached server, one-shot job
podman run -it --name lab alpine sh
podman run -d --name web -p 8080:80 nginx:alpine
podman run --rm -v "$PWD":/data -w /data golang:alpine go build .
# The daily loop
podman ps -a
podman logs -f web
podman exec -it web sh
podman inspect web --format '{{.State.Status}}'
podman stop web && podman rm web
Why the flags work the way they do: -p 8080:80 publishes host port 8080 to the container's 80 (rootless can't bind ports below 1024 without extra config — see troubleshooting); -v "$PWD":/data bind-mounts the current directory read-write; -e injects environment variables; --rm cleans up after a one-shot; --restart=always delegates restart policy to systemd on the host. podman inspect is your JSON oracle for anything the CLI doesn't print.
Images, builds, and registries
podman build uses the Buildah library under the hood — same Dockerfile, no daemon. Pulling from Docker Hub works out of the box (docker.io is in the default search list on Debian/Ubuntu/Fedora).
podman search --limit 10 ffuf
podman pull docker.io/library/alpine:latest
podman images
podman image history alpine
podman image tree alpine
# Build from a Dockerfile in the current directory
podman build -t mylab:latest .
podman rmi mylab:latest
Volumes and networks
Named volumes survive container removal; user-defined networks get DNS via aardvark-dns, so containers can reach each other by name — the same behavior Docker's bridge gives you.
podman volume create labdata
podman run -d --name db -v labdata:/var/lib/postgresql/data postgres:16
podman network create labnet
podman run -d --name api --network labnet mylab:latest
podman run --rm --network labnet curlimages/curl curl -s http://api:8080/health
podman network inspect labnet
Networking under the hood is netavark (default since Podman 4.0, CNI removed in 5.0) plus aardvark-dns for name resolution — check yours with podman info --format '{{.Host.NetworkBackend}}'.
Pods: the Kubernetes-shaped grouping
A pod is a group of containers sharing a network namespace — they talk over localhost and die together. This is the model that makes podman play kube trivial later.
podman pod create --name webstack -p 8080:80
podman run -d --pod webstack --name nginx nginx:alpine
podman run -d --pod webstack --name redis redis:7
podman pod ps
podman pod inspect webstack
Containers as systemd Services: Quadlet (Podman's Superpower)
Docker's answer to "start on boot" is a daemon restart policy. Podman's answer is systemd itself — the same init that supervises your SSH daemon now supervises your containers, with journald logs, dependencies, and timers for free.
The old way (podman generate systemd --name web) still works but is deprecated in favor of Quadlet (integrated since Podman 4.4): you drop a declarative .container file into ~/.config/containers/systemd/, and a systemd generator turns it into a unit. No scripts, no generated blobs to maintain.
mkdir -p ~/.config/containers/systemd
cat > ~/.config/containers/systemd/web.container <<'EOF'
[Unit]
Description=Lab web server
[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80
Volume=./labdata:/usr/share/nginx/html:Z
[Service]
Restart=on-failure
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now web.service
systemctl --user status web.service
journalctl --user -u web.service -f
Two details worth knowing: Quadlet also understands .volume, .network, .pod and .kube files for the same declarative treatment, and on Podman 5.6+ you can install a Quadlet with podman quadlet install web.container instead of copying it by hand.
Surviving logout: lingering
User services die when you log out — unless you enable lingering, which tells systemd to keep the user's manager running without a session. One command, run as root:
sudo loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=Linger
Compose Compatibility: Be Honest About the Limits
Two different things get called "Podman compose," and the difference matters:
podman-compose— a Python reimplementation of the Compose spec that shells out to thepodmanCLI. No socket needed, works rootless out of the box, but lags Docker Compose on advanced features.podman compose— a thin wrapper that calls an external compose provider (docker-compose or podman-compose). If docker-compose is installed, it wins; override withPODMAN_COMPOSE_PROVIDER.
# Option A: python podman-compose
sudo dnf install -y podman-compose
podman-compose up -d
# Option B: official docker-compose talking to the Podman socket
sudo dnf install -y docker-compose
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
podman compose up -d
The honest limits: rootless networking constrains some Compose features (static IPs, host networking), SELinux hosts need :z/:Z on bind-mount volumes or you'll hit permission denials, and podman-compose doesn't implement every Compose v2 directive. For anything pod-shaped, podman play kube is often the cleaner path than forcing Compose semantics.
Hardening Rootless Containers: the Security Framing
Rootless is already a head start: a compromised container process is an unprivileged user on the host, not root. But "rootless" isn't "hardened" — layer the standard controls on top. This matters double when you run pentest tooling in containers: only ever run those labs on infrastructure you own or are explicitly authorized to test.
# Drop every capability, then add back only what's needed
podman run --rm --cap-drop ALL --cap-add NET_RAW alpine sh
# Map your host UID into the container (files you write stay yours)
podman run --rm --userns=keep-id -v "$PWD":/work alpine sh -c 'id && touch /work/test'
# Read-only rootfs + no new privileges
podman run --rm --read-only --security-opt no-new-privileges nginx:alpine
Beyond flags: Podman applies a default seccomp profile (/usr/share/containers/seccomp.json) on every container, Debian/Ubuntu layer AppArmor on top, and Fedora runs SELinux enforcing — which is why Fedora's rootless default is genuinely the strongest of the three. On SELinux systems, bind-mounted volumes need relabeling: :z shares the volume's label across containers, :Z gives the container a private label. Forget it and you get permission-denied writes that look like chmod problems. Image signature policy lives in /etc/containers/policy.json (default: accept anything — tighten it for production).
Troubleshooting Rootless Podman
These are the walls I've hit, in order of frequency:
- "operation not permitted" on first run — subuid/subgid missing. Fix:
usermod --add-subuids+podman system migrate. - cgroup errors — Podman wants cgroups v2 with the systemd cgroup manager. Verify with
podman info --format '{{.Host.CgroupVersion}}'; on odd setups try--cgroup-manager=cgroupfs. - "slirp4netns not found" or pasta weirdness — install
passtorslirp4netns; switch backends incontainers.confwithdefault_rootless_network_cmd. - Can't bind port 80 — rootless processes can't bind below 1024. Use a high port, or if you really need 80, set
net.ipv4.ip_unprivileged_port_start=80(as root, system-wide). - Volume permission denials on Fedora — SELinux. Add
:z/:Zto the mount, don't chase chmod ghosts. - Containers die on logout —
sudo loginctl enable-linger "$USER".
Podman on Termux (Android, No Root)
Here's where the internet gets sloppy, so let's be precise. I checked the termux-packages repo directly: there is no podman package — not in packages/, not in root-packages/. The package request issue (#9141) was closed the same day it was opened, and the Podman maintainers themselves confirmed there are no official Android builds. Anyone telling you to run pkg install podman is describing a package that doesn't exist in the official repos.
That leaves three realistic paths, in order of reliability.
Path 1: proot-distro + Podman inside a real distro (on-device)
proot-distro installs a full Debian/Ubuntu/Kali rootfs that runs under proot — a ptrace-based userspace that fakes chroot, mount and UID remapping without root. Install the distro, then apt install podman inside it:
pkg update && pkg install -y proot-distro
proot-distro install debian
proot-distro login debian
apt update && apt install -y podman
podman --version
# Simple tool containers can work:
podman run --rm docker.io/library/alpine echo "hello from proot"
The honest caveats, because they matter: proot is not kernel isolation. No real user/PID/network namespaces, no cgroups, no seccomp — proot is path translation via ptrace, with a real performance tax on syscall-heavy work. "Containers" under proot work for running tools and CTF labs, but they are not a security boundary, and anything needing real namespaces (nested container runtimes, network namespaces) will fail or misbehave. Also: no systemd inside proot, so Quadlet is off the table there.
Why Podman maps to proot better than Docker does: Docker's daemon fundamentally wants root and kernel namespaces, so under proot it mostly fails at container creation. Podman's rootless model — unprivileged user, no daemon, CLI-driven — is the closest thing to a fit, which is exactly why it's the engine to try on-device. Expect it to work for simple images and to be slow; treat it as a lab toy, not infrastructure.
Path 2: Remote context — control a real host from the phone (recommended)
This is the reliable Termux story: install Podman on a PC or server, then point your phone's podman CLI at it over SSH. The phone becomes a thin client for a real container engine — the same pattern as docker context, but with Podman's native system connection machinery.
# The podman CLI itself doesn't ship in Termux — run it from proot-distro
# (see Path 1), or from a static build. The connection itself is SSH:
pkg install -y openssh
proot-distro login debian -- apt install -y podman
# From inside the proot distro (or any podman CLI):
podman system connection add labbox \
ssh://user@192.168.1.50:22/run/user/1000/podman/podman.sock
podman system connection list
podman --remote ps -a
On the host side, the rootless socket must be exposed over SSH — one-time setup on the PC or server:
# On the container host (as the same unprivileged user):
systemctl --user enable --now podman.socket
sudo loginctl enable-linger "$USER"
# Confirm the socket is listening:
ss -lx | grep podman
Rootful hosts use ssh://root@host/run/podman/podman.sock instead. Once the connection is set, every podman command runs on the remote host — pull, run, exec, logs, everything.
Path 3: Community builds (experimental, not official)
Community projects like kenhys/termux-podman build Podman for Termux outside the official repos. They exist, they're interesting, and they're unsupported — expect breakage on storage drivers (you'd need --storage-driver=vfs, since fuse-overlayfs isn't available) and networking (pasta/slirp4netns don't run cleanly on Android's kernel). Try it for the challenge; don't build a workflow on it.
The realistic lab
For pentest and CTF work, the pattern that actually holds up: run the heavy tool images (nmap, ffuf, gobuster, metasploit) on a PC or VPS with rootless Podman, and drive them from Termux over the remote connection. On-device, use proot-distro for quick tool isolation — it's a chroot with extra steps, not a container, and that's fine for a CTF box. Either way: only point this at targets you own or are authorized to test.
Podman vs Docker: Pros and Cons
| Dimension | Podman | Docker |
|---|---|---|
| Architecture | Daemonless fork/exec (conmon + crun) | Root daemon + API socket |
| Rootless | First-class, default posture | Possible but bolted on (rootless mode) |
| Resource footprint | No daemon; containers are child processes | Daemon always resident |
| systemd integration | Quadlet, generate systemd, pods as units | Restart policies; no native units |
| Ecosystem / adoption | Growing; Docker-compatible CLI | Industry default; every CI/CD speaks it |
| Kubernetes | play kube / generate kube | Swarm (legacy); k8s via third-party |
| Compose | Wrapper or Python reimplementation | Native, mature plugin |
| Migration effort | alias docker=podman covers most | — |
| GPU / integrations | CDI + nvidia-container-toolkit | Mature nvidia-docker path |
| Restart semantics | systemd owns lifecycle; no daemon to wedge | Daemon restart restarts containers (and can wedge) |
Pick Podman for: Linux-only labs, rootless security posture, systemd-managed services, anything Kubernetes-bound, and low-footprint hosts. Stay on Docker for: team environments standardized on Docker Compose, macOS/Windows desktops (Docker Desktop is still smoother there), and any tooling that hard-depends on the Docker daemon API. The migration cost is one alias and a weekend — most people who switch don't go back.
Takeaways
- Daemonless is the security win — no root daemon on a socket means a whole class of host attacks disappears; rootless means a popped container is an unprivileged user, not root.
- Quadlet turns containers into systemd units — declarative
.containerfiles in~/.config/containers/systemd/plusloginctl enable-lingergive you boot-persistent, journald-logged services with zero scripts. - Rootless has three prerequisites — subuid/subgid ranges, a user-space network stack (pasta or slirp4netns), and overlay support. Diagnose failures in that order.
- Termux's realistic Podman is remote — no official package exists; use proot-distro for on-device experiments (knowing it's not real isolation) and
podman system connection addto drive a real host from the phone.
Anchor insight: Podman isn't a Docker clone — it's the same container format with a fundamentally safer execution model, and once you internalize "no daemon, systemd owns the lifecycle," the entire toolchain (pods, Quadlet, play kube) clicks into place.
Related reading on openlinuxlab: Termux on Android (Non-Root): Complete Setup Guide · SSH Hardening Guide · Netcat Usage & Examples — the Termux and CLI foundations this tutorial builds on.
