// learn Β· linux Β· rhcsa Β· 10 min

Podman containers.

Podman is RHEL's container engine β€” drop-in compatible with the docker CLI, but daemonless and rootless by default. Watch a container go from `podman run` to a persistent systemd unit via Quadlet, the modern way to make rootless services survive reboots.

Podman containers animated tutorial. Daemonless and rootless containers, podman pull/run/exec/logs/ps/inspect, volumes and port publishing, the difference from Docker, and persisting containers as systemd units with Quadlet. RHCSA EX200 ready.
// podman containers

Daemonless, rootless, and Docker-CLI compatible. Watch a container go from pull β†’ run β†’ exec β†’ persistent systemd unit (Quadlet).

// before you start
you should know
  • Β·Vague familiarity with containers / Docker
  • Β·The systemd-targets / units lesson
  • Β·Comfortable running commands as a normal user
by the end you'll

Pull, run, exec, log into containers; mount volumes correctly on SELinux; understand rootless mode; persist with Quadlet.

pace: 10 minutes

podman β€” step 1 / 6 Β· vs docker
$ podman ps
no containers yet β€” start with `podman pull` + `podman run`

Daemonless, rootless β€” what makes podman different

Docker runs a single privileged daemon (dockerd) that owns every container. Podman doesn't: each `podman run` is a normal process owned by your user, supervised by conmon. Containers run rootless by default β€” your UID inside the container maps to your UID outside, no root needed. Same CLI verbs as docker (`alias docker=podman` usually just works), but no daemon to crash, no root to compromise.

$podman version
$alias docker=podman
$podman info
// key insight

Three things separate Podman from Docker on RHEL: no daemon, rootless by default, and first-class systemd integration via Quadlet. The first two improve security; the third is what makes containers feel native β€” they show up in `systemctl status`, log to the journal, and survive reboots like any other service.

// exam-ready Β· podman + quadlet
$podman pull IMG
$podman run -d --name N -p H:C IMG
$podman run --rm -it IMG sh
$podman ps / ps -a
$podman images
$podman exec -it N bash
$podman logs -f N
$podman inspect N | jq .
$podman stop / start / restart / rm N
$podman rmi IMG
$-v /host:/container:Z
$podman volume create / ls / rm
$~/.config/containers/systemd/X.container
$systemctl --user daemon-reload
$systemctl --user enable --now X.service
$loginctl enable-linger USER
// check yourself
4 quick questions
Q1

Biggest architectural difference between Podman and Docker?

Q2

You bind-mount /srv/html into a container on RHEL with SELinux enforcing. The container can't read the files. What's the fix?

Q3

You want a rootless container to survive reboots. Best modern approach on RHEL 9?

Q4

A rootless `podman run -p 80:80 nginx` fails with a permission error. Why, and the cleanest workaround?

These aren't graded β€” they're just for active recall, which is what actually makes the lesson stick.

🌳
// where Quadlet plugs in

systemd targets & units

Quadlet turns containers into systemd .service units. Solidify how units + dependencies + targets work and Quadlet stops feeling magical.

open β†’

Read this lesson as text6 chapters Β· quiz answers

  1. 01Daemonless, rootless β€” what makes podman different

    Docker runs a single privileged daemon (dockerd) that owns every container. Podman doesn't: each podman run is a normal process owned by your user, supervised by conmon. Containers run rootless by default β€” your UID inside the container maps to your UID outside, no root needed. Same CLI verbs as docker (alias docker=podman usually just works), but no daemon to crash, no root to compromise.

    • $ podman version β€” client + engine versions
    • $ alias docker=podman β€” shim for muscle memory
    • $ podman info β€” rootless?, storage driver, ...
  2. 02pull and run

    podman pull docker.io/library/nginx:1.25 fetches the image (registries.conf decides search order β€” quay.io is often default on RHEL). podman run --name web -d -p 8080:80 nginx:1.25 starts a detached container, publishes host port 8080 β†’ container 80. --rm removes the container when it stops. -it for interactive shells. The image is layered, immutable; the running container is a writable layer on top.

    • $ podman pull docker.io/library/nginx:1.25 β€” fetch image
    • $ podman run --name web -d -p 8080:80 nginx:1.25 β€” detached + port
    • $ podman run --rm -it alpine sh β€” interactive, auto-rm
    • $ podman images / podman ps β€” what's pulled / running
  3. 03exec, logs, inspect

    Don't bake debug tools into the image β€” podman exec -it web bash drops a shell inside the running container. podman logs -f web tails its stdout/stderr (whatever the app writes there). podman inspect web dumps the full config + state as JSON β€” pipe through jq for one field. podman stop / start / restart / rm complete the lifecycle.

    • $ podman exec -it web bash β€” shell in a running container
    • $ podman logs -f web β€” tail logs
    • $ podman inspect web | jq '.[0].NetworkSettings' β€” one field of inspect
    • $ podman stop web && podman rm web β€” shut down + remove
  4. 04Volumes β€” keeping data when the container goes

    Containers are ephemeral; their writable layer dies with them. Persistent state lives in VOLUMES (managed by podman) or BIND MOUNTS (a host path mounted in). -v /host/path:/container/path:Z bind-mounts; the :Z tells SELinux to relabel for the container (critical on RHEL). -v myvol:/var/lib/data uses a named volume managed by podman. List volumes with podman volume ls.

    • $ podman run -d -p 8080:80 -v /srv/html:/usr/share/nginx/html:Z nginx:1.25 β€” bind mount with SELinux relabel
    • $ podman volume create webdata β€” named volume
    • $ podman run -v webdata:/var/lib/data alpine β€” use it
    • $ podman volume ls / inspect / rm β€” manage
  5. 05Rootless β€” and where files actually live

    Running as a normal user, your container images and volumes live under ~/.local/share/containers/storage, not /var/lib/containers. UID 0 inside maps to YOUR uid outside (subuid/subgid ranges handle additional UIDs). One gotcha: rootless containers can't bind to host ports < 1024 by default β€” use -p 8080:80 (high host port) and reverse-proxy, or grant the cap via /etc/sysctl.d for that range.

    • $ podman info | grep -i rootless β€” confirm rootless mode
    • $ cat /etc/subuid /etc/subgid β€” your UID range
    • $ loginctl enable-linger $USER β€” keep services running after logout
    • $ sysctl net.ipv4.ip_unprivileged_port_start=80 β€” allow rootless :80 (cautiously)
  6. 06Quadlet β€” containers as systemd units

    To make a rootless container survive reboots, run it as a systemd USER service. The modern way is QUADLET: drop a .container file in ~/.config/containers/systemd/ describing the image + options, and systemd auto-generates a service unit for you. Then systemctl --user enable --now web.service. Reload with systemctl --user daemon-reload after edits. Older docs use podman generate systemd β€” Quadlet is the newer, cleaner replacement.

    • $ ~/.config/containers/systemd/web.container β€” Quadlet file (Image=, PublishPort=, etc.)
    • $ systemctl --user daemon-reload β€” regenerate units from Quadlets
    • $ systemctl --user enable --now web.service β€” start + autostart
    • $ systemctl --user status web.service β€” verify
    • $ loginctl enable-linger $USER β€” user services run without login

Quiz β€” answers

Biggest architectural difference between Podman and Docker?
β†’Docker uses a privileged daemon (dockerd) that owns all containers. Podman is daemonless β€” each container is a process owned by your user, supervised by conmon β€” and runs rootless by default.No daemon means no single point of failure, no privileged service to compromise, and easier rootless operation. Image format and CLI verbs match Docker, so most workflows port over directly.
You bind-mount /srv/html into a container on RHEL with SELinux enforcing. The container can't read the files. What's the fix?
β†’Add :Z to the volume option β€” -v /srv/html:/usr/share/nginx/html:Z β€” so podman relabels the directory's SELinux context for container access.Without :Z (or :z for shared), the host directory keeps a context the container's process type can't read. :Z relabels for THIS container; :z is shared (multiple containers). Disabling SELinux is the wrong fix.
You want a rootless container to survive reboots. Best modern approach on RHEL 9?
β†’Write a Quadlet (.container file) under ~/.config/containers/systemd/, run systemctl --user daemon-reload, then systemctl --user enable --now NAME.service. Also run loginctl enable-linger $USER so user services run without an active login.Quadlet is the modern Podman + systemd integration; it superseded podman generate systemd. The linger step is critical β€” without it, user services stop when you log out.
A rootless podman run -p 80:80 nginx fails with a permission error. Why, and the cleanest workaround?
β†’Rootless containers can't bind to host ports below 1024 by default; map to a high host port like -p 8080:80 and reverse-proxy, or lower net.ipv4.ip_unprivileged_port_start via sysctl.Privileged ports (<1024) are a kernel restriction, not a podman one. Either pick a high host port (cleanest) or globally lower the threshold with sysctl net.ipv4.ip_unprivileged_port_start=80 (persist in /etc/sysctl.d) if you really need :80.
// more in linux

keep going β€” these pair well with what you just learned.

see all linux β†’

See also

back to RHCSA trackall lessons