Podman Basics

Daemonless, rootless containers on Linux. Parity with Docker where it matters, plus pods, Quadlet, podman auto-update, pasta vs slirp4netns, and the debug commands that pay for themselves.

If you only remember six things
  • Podman is daemonless — every container is a child of the user who ran podman. No dockerd. No shared root socket.
  • Rootless is the default. Fix it by configuring /etc/subuid and /etc/subgid once per user.
  • On modern systemd, use Quadlet, not podman generate systemd.
  • A pod is a shared network/IPC namespace for a set of containers. It is the unit Kubernetes inherited.
  • pasta replaces slirp4netns as the default rootless network stack on current distros. It is faster and more correct.
  • Auto-updates are opt-in via the io.containers.autoupdate label plus the podman-auto-update.timer.

Docker parity and the real differences

95% of the Docker CLI works unchanged under Podman. You can alias docker=podman and most scripts will keep running. The meaningful differences are architectural rather than syntactic:

ConcernDockerPodman
ArchitectureClient + long-running dockerd daemonFork/exec: podman launches conmon, which supervises crun/runc
Root requiredHistorically yes; rootless mode existsRootless by default
User namespaceOpt-inAlways on in rootless mode
PodsNo native conceptFirst-class podman pod
systemd(Third-party tools)podman generate systemd or Quadlet .container files
ComposeBuilt in (docker compose)External (podman-compose) or use native Quadlet
API socketDefault on; SUID-like access via the docker groupOpt-in per-user via podman.socket systemd unit

Rootless: subuid, subgid, podman unshare

Rootless Podman runs each container inside a user namespace. Inside the container, uid=0 is mapped to your host UID; a block of sub-UIDs is mapped to fake other UIDs inside the container. The mapping is configured once:

# Check
cat /etc/subuid /etc/subgid | grep "$USER"
# Example:
# alice:100000:65536
# (alice owns host UIDs 100000..165535 to hand out to containers)

# If empty — add them (root, one-time):
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 alice

# After editing /etc/subuid or /etc/subgid, tell Podman to reinit:
podman system migrate

When you need to work inside the rootless UID mapping — for example to fix ownership on a bind-mount directory that the container writes to — use podman unshare:

# A file chowned to uid=1000 inside a rootless container lives at
# uid=100999 on the host. You can't chmod it directly as $USER:
$ ls -ln ./data
-rw-r--r-- 1 100999 100999 0 Mar 12 12:00 db.sqlite

$ podman unshare chown -R 1000:1000 ./data
# Inside the unshared namespace, 1000 maps to host 100999. Fixed.
Rootless can't bind < 1024. Unprivileged users cannot bind low-numbered ports. Either publish on a high port (-p 8080:80 and front with a reverse proxy), or grant the capability: sudo sysctl net.ipv4.ip_unprivileged_port_start=80.

Pods: the unit Kubernetes inherited

A pod is a group of containers that share a network, IPC, and UTS namespace. Everything in a pod can talk to everything else on localhost, they see the same hostname, and they are treated as a unit for start/stop.

# Create a pod that publishes port 8080
podman pod create --name web --publish 8080:80

# Add nginx — it binds on port 80 inside the pod's netns
podman run -d --pod web --name nginx nginx:1.27-alpine

# Add a sidecar that reads nginx logs via a shared volume
podman run -d --pod web --name logshipper \
    -v nginx-logs:/var/log/nginx:ro \
    vector:latest

podman pod ps
podman pod logs web
podman pod stop web; podman pod rm web

This is the same mental model as a Kubernetes pod: the "infra" container holds the namespaces, the other containers join them. If you understand Podman pods, you already understand 80% of why pods exist in Kubernetes.

Systemd integration: Quadlet

Historically you would run podman generate systemd to emit unit files. It works, but the generated files are verbose and hard to edit. On systemd 250+ (which means any current distro), use Quadlet instead — declarative .container files that systemd's generator translates into real units at boot.

# ~/.config/containers/systemd/caddy.container
[Unit]
Description=Caddy reverse proxy
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/caddy:2.8-alpine
AutoUpdate=registry
PublishPort=443:443
PublishPort=80:80
Volume=caddy-data:/data
Volume=%h/caddy/Caddyfile:/etc/caddy/Caddyfile:Z

[Service]
Restart=always

[Install]
WantedBy=default.target

Activate:

systemctl --user daemon-reload
systemctl --user start caddy.service
systemctl --user enable caddy.service

# Enable lingering so the user's units start at boot without a login
loginctl enable-linger alice

You can write .pod, .volume, .network, .kube (full Kubernetes YAML), and .image Quadlet files similarly. They all live under ~/.config/containers/systemd/ for user services or /etc/containers/systemd/ for system services.

Quadlet vs podman generate systemd. The generator is still supported but deprecated for new installs. Quadlet is roughly half the lines, survives Podman upgrades more gracefully, and integrates with systemd natively.

Volumes and bind mounts

Two mechanisms; they look similar and behave differently.

podman volume create pgdata
podman run -d --name db \
  -e POSTGRES_PASSWORD=changeme \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

# Bind mount into a rootless container — SELinux needs a label.
# Append :Z for private, :z for shared.
podman run --rm -it -v "$PWD:/work:Z" alpine sh
Permission errors on bind mounts in rootless are almost always the user namespace mapping. The container's uid=1000 is host uid=101000. Either podman unshare chown the host path, or use a named volume.

Networking: slirp4netns vs pasta

Rootless containers can't create real network interfaces on the host — only root can — so Podman ships a user-space network stack:

podman info --format '{{.Host.NetworkBackend}} / {{.Host.RootlessNetworkCmd}}'
# netavark / pasta    <- modern
# cni / slirp4netns   <- older systems

# Pin per-run if needed:
podman run --network=pasta:...  image
podman run --network=slirp4netns image

Rootful Podman (or rootless with a CNI plugin) creates real bridges and works just like Docker's default. The user-space stacks are only for rootless.

Auto-updating images

Podman can poll a registry and rollout newer images for containers you opt in. Label the container and enable the timer:

# Label at run time (or in Quadlet: AutoUpdate=registry)
podman run -d --name caddy \
  --label "io.containers.autoupdate=registry" \
  caddy:2.8-alpine

# Enable the update timer (system or --user)
systemctl --user enable --now podman-auto-update.timer

# What would be updated right now?
podman auto-update --dry-run

# Force a check immediately
podman auto-update

Values the label accepts:

Rollback happens automatically if the new container fails to start within the service's RestartSec. Combined with registry auth via OIDC this gives a hands-off upgrade path for homelab and small-shop deployments.

Logging drivers

podman info --format '{{.Host.LogDriver}}'

# Set per-run
podman run --log-driver=journald --log-opt tag=myapp image

# Default in ~/.config/containers/containers.conf
[containers]
log_driver = "journald"

Options worth knowing:

Debugging: inspect, top, events

The day-to-day "what is this container actually doing" commands:

# All runtime state: entrypoint, env, mounts, networks, uidmap
podman inspect web

# Processes running inside, with host PIDs
podman top web huser,pid,etime,pcpu,pmem,args

# Live resource use
podman stats --no-stream

# A firehose of lifecycle events (start, stop, OOM, health)
podman events --since 1h

# Open a shell in a running container
podman exec -it web sh

# Copy a file out for offline analysis
podman cp web:/var/log/nginx/access.log ./access.log

# Reproduce exactly what Podman ran (the OCI runtime invocation)
podman inspect web --format '{{json .Config}}' | jq

For deeper problems — container won't start at all, port binding confusion, userns misconfiguration — the debug pair is:

# What would Podman do, without doing it
podman run --log-level=debug --rm -it image cmd 2>&1 | head -40

# What does the system think the container's network looks like
podman unshare --rootless-netns ss -tlnp

Next up: Docker Compose for declarative multi-container local dev (Podman can run the same compose.yaml files), or jump to Kubernetes Light when one host isn't enough.