Skip to content

Podman — the 80/20 course for RHEL hosts

Scope: run and build OCI containers on a RHEL (or Fedora) host with Podman 5.0+. Daemonless, rootless-by-default, OCI-compliant, Docker-CLI-compatible. Kubernetes-on-Podman beyond podman play kube, Buildah deep dive, Skopeo deep dive, Quadlet (see module 10, Quadlet), and Podman Desktop are intentionally out of scope — pointers at the end.

Read this top-to-bottom once, then come back to specific sections as reference. This module's glossary is the dictionary for any term you don't recognise here — it also seeds the Retain flashcards.


TL;DR — in 30 seconds:

  • Podman is Docker's RHEL-native drop-in: daemonless (conmon per container, no dockerd), rootless by default, and systemd-first — alias docker=podman covers ~95% of the CLI you already know.
  • Rootless works by mapping container UID 0 to an unprivileged host UID via /etc/subuid//etc/subgid, so container "root" can never write anywhere the host user couldn't already write.
  • For anything that must survive a reboot, skip the deprecated podman generate systemd; the current systemd-native answer is a Quadlet .container unit (module 10).

1. Why Podman — and when not to use it

Podman is the container engine RHEL ships by default. It runs OCI containers the same way Docker does, with three architectural differences that matter:

  • Daemonless. Every podman invocation is a self-contained process. No dockerd, no socket, no "service not running" failure mode, no daemon attack surface.
  • Rootless by default. Containers run as unprivileged users via Linux user namespaces. No root inside, no root outside. Eliminates a whole class of escape-via-root exploits.
  • Systemd-first. Designed to integrate with systemd (via Quadlet, see module 10, Quadlet) rather than to be its own init system. Each container is just a process the host can supervise.

What you get over Docker on RHEL:

  • No systemctl restart docker after host upgrades. Nothing to restart.
  • Drop-in CLI. alias docker=podman works for ~95% of commands you've muscle-memorised.
  • Native pods. podman pod create gives you the Kubernetes pod primitive locally, single-host.
  • podman play kube. Take a Kubernetes YAML, run it locally as pods + containers, without a cluster.
  • OCI-only. No Docker-specific extensions to learn or avoid.

When not to reach for Podman:

  • You're on Ubuntu / Debian and the rest of the org runs Docker → Docker still works fine; the model differences only matter if you're going rootless.
  • You need a cluster orchestrator → Podman is single-host. Kubernetes is the orchestrator; Podman is the engine the orchestrator could call.
  • You need Docker Swarm → it's deprecated regardless; choose Kubernetes (k3s for tiny) instead.
  • You depend on a tool that talks to the Docker socket → start podman system service to expose a Docker-API socket, or accept the rewrite.

The 80/20 sweet spot Podman nails: container engine for a single RHEL host, especially when you want rootless, when you want systemd-native, and when you don't want a long-running daemon.


2. Mental model — daemonless and rootless

DOCKER (daemonful):
+----------------+  socket   +----------------+   spawn   +-------------+
|  docker CLI    | --------> |  dockerd       | --------> |  container  |
|  (user, root)  |           |  (long-running |           |  process    |
|                |           |   daemon, root)|           |  (root)     |
+----------------+           +----------------+           +-------------+

PODMAN (daemonless, rootless):
+----------------+   fork    +-------------+   spawn   +-------------+
|  podman CLI    | --------> |  conmon     | --------> |  container  |
|  (your user)   |           |  (per-ctr   |           |  process    |
|                |           |   monitor)  |           |  (your uid  |
+----------------+           +-------------+           |   mapped    |
                                                       |   to subuid)|
                                                       +-------------+

Three things to internalise:

  1. No daemon. podman run forks conmon (a tiny per-container monitor) and the container process, then podman exits. conmon keeps the container running and reports back to whoever cares (your shell, or systemd via Quadlet).
  2. Rootless via user namespaces. Your unprivileged UID (say 1000) gets allocated a range of "subordinate" UIDs (/etc/subuid: 1000:100000:65536). Inside the container, UID 0 (root) maps to host UID 100000 — a UID the host treats as unprivileged. Container "root" can't write anywhere the host user can't already write.
  3. State is per-user. Rootless storage lives in ~/.local/share/containers/; rootful in /var/lib/containers/. podman ps as user A doesn't show user B's containers; they're literally different storage trees.

The daemonless model has one cost: there's nothing to "pause and inspect later". If you want a container to survive podman exiting and a reboot, you need either --restart=always plus the podman-restart.service enabled — and, for rootless, loginctl enable-linger $USER so the user's services start at boot rather than at next login — or, much better, a Quadlet .container unit (covered in module 10, Quadlet).


3. Setup

# RHEL 9 / Fedora — Podman is in the base repos
sudo dnf install -y podman

# macOS — Podman runs in a small Linux VM
brew install podman
podman machine init
podman machine start

# Sanity
podman version                        # 5.x
podman info                           # storage driver, runtime, registries

On RHEL, no further setup is needed. On macOS, podman machine manages the underlying VM; it auto-starts on podman commands if podman machine start is in your shell init.

For rootless to work on Linux, every user that runs containers needs subordinate UID/GID ranges. On modern distros this is set automatically when you useradd. Verify:

grep $USER /etc/subuid                # e.g. tdeheurles:100000:65536
grep $USER /etc/subgid                # same range for GIDs

If empty, allocate manually:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
podman system migrate                 # rebuild rootless storage

Optional: register the Docker-compat socket so tools that talk to /var/run/docker.sock see a Podman-served API instead:

systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"

4. Your first container

podman run --rm -it docker.io/library/alpine:3.19 sh
/ # cat /etc/os-release
NAME="Alpine Linux"
/ # exit

podman run -d --name nginx -p 8080:80 docker.io/library/nginx:1.25
curl http://localhost:8080
podman ps
podman logs nginx
podman stop nginx
podman rm nginx

A few things to notice immediately:

  • Full image URL. docker.io/library/alpine:3.19, not just alpine. Podman defaults to a curated registry list in /etc/containers/registries.conf; pulling a bare name fails on stricter configs. Get used to typing the full path.
  • -p 8080:80 is host:container. Same as Docker.
  • Rootless ports < 1024. Try -p 80:80. On a default rootless setup, it fails. Either bump the host port (-p 8080:80), set sysctl net.ipv4.ip_unprivileged_port_start=80, or grant CAP_NET_BIND_SERVICE to rootlesskit.
  • podman ps only shows your containers. A second shell as the same user shares the view; a different user sees nothing.

5. Images

Images come from a registry. Pull, list, tag, push, remove:

podman pull registry.access.redhat.com/ubi9/ubi:latest
podman pull quay.io/podman/hello:latest
podman pull docker.io/library/postgres:16-alpine

podman images                              # list
podman images --digests                    # show content digests
podman inspect ubi9/ubi:latest             # full metadata
podman tag ubi9/ubi:latest myregistry.example.com/baseos:1.0
podman push myregistry.example.com/baseos:1.0
podman rmi quay.io/podman/hello:latest

Authentication for private registries:

podman login registry.example.com
# Username: ...
# Password: ...
# Login Succeeded!

podman login --username CI --password "$REGISTRY_TOKEN" ghcr.io
podman logout registry.example.com

Credentials persist in $XDG_RUNTIME_DIR/containers/auth.json (rootless) — a JSON file you can edit by hand or templated into CI runners.

Pin to digests in production. A tag like postgres:16-alpine is mutable — the image behind it changes when the upstream rebuilds. To get bit-exact reproducibility, pin to a digest:

podman pull docker.io/library/postgres:16-alpine
podman inspect docker.io/library/postgres:16-alpine --format '{{.Digest}}'
# sha256:abc123...

# In your Containerfile / compose / Quadlet:
docker.io/library/postgres@sha256:abc123...

Save and load images without a registry (handy for air-gapped setups):

podman save -o postgres-16.tar docker.io/library/postgres:16-alpine
scp postgres-16.tar isolated-host:
ssh isolated-host podman load -i postgres-16.tar

6. Building images with Containerfile

Containerfile is the same format as Dockerfile; Podman reads either filename. A practical, small Containerfile:

# Stage 1: build
FROM registry.access.redhat.com/ubi9/nodejs-20:latest AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

# Stage 2: runtime
FROM registry.access.redhat.com/ubi9/nodejs-20-minimal:latest
WORKDIR /app

# Run as non-root inside the container
USER 1001

# Copy build artifacts and dependencies
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist         ./dist
COPY --from=build /app/package.json ./

ENV NODE_ENV=production \
    PORT=3000

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/healthz || exit 1

CMD ["node", "dist/server.js"]

Build and inspect:

podman build -t demo-app:1.0 -f Containerfile .
podman build -t demo-app:1.0 --target build .                # stop at the build stage
podman build -t demo-app:1.0 --platform linux/amd64 .        # cross-arch
podman build -t demo-app:1.0 --build-arg VERSION=1.0 .

podman images demo-app
podman inspect demo-app:1.0
podman run --rm demo-app:1.0

Multi-arch builds via buildx-style commands:

podman manifest create demo-app:1.0
podman build --platform linux/amd64       --manifest demo-app:1.0 .
podman build --platform linux/arm64       --manifest demo-app:1.0 .
podman manifest push demo-app:1.0 docker://myregistry.example.com/demo-app:1.0

Two Containerfile rules that bite:

  1. USER matters. RHEL UBI images run as UID 1001 by default. Files copied from the build stage are owned by root unless you --chown=1001:1001 or chown after copying. Permission-denied at runtime almost always traces back to this.
  2. Cache busts on COPY. COPY . . followed by RUN npm ci re-installs deps every time any file changes. Two-step COPY (package*.json first, then npm ci, then everything) keeps the deps layer cached.

7. Container lifecycle

The whole vocabulary, in the order you'll use it:

# Create and start in one step
podman run -d --name demo -p 8080:3000 demo-app:1.0

# Create now, start later
podman create --name demo -p 8080:3000 demo-app:1.0
podman start demo

# Inspect
podman ps                              # running
podman ps -a                           # all (incl. stopped)
podman logs demo                       # all output
podman logs -f demo                    # follow
podman logs --tail 50 demo             # last 50 lines
podman inspect demo                    # full JSON
podman inspect demo --format '{{.State.Status}}'
podman top demo                        # processes inside
podman stats demo                      # live CPU/mem/net/io

# Interact
podman exec -it demo /bin/sh           # shell inside
podman exec demo printenv              # run one command
podman cp ./local-file demo:/tmp/      # copy in
podman cp demo:/var/log/app.log .      # copy out
podman attach demo                     # reattach stdio (Ctrl-P Ctrl-Q to detach)

# Stop / remove
podman stop demo                       # SIGTERM, wait, SIGKILL
podman stop --time 30 demo             # grace period
podman kill demo                       # immediate SIGKILL
podman restart demo
podman rm demo
podman rm -f demo                      # stop then remove

The flags worth memorising on podman run:

Flag What it does
-d / --detach Don't attach stdio; print the container ID.
--name <n> Give the container a name; required for any later start/stop.
-p host:container[/proto] Publish a port (bridge networks).
-v name:/path / -v /host:/ctr Mount a named volume or a bind mount.
-e KEY=VAL Set an env var inside the container.
--env-file .env Load env from a dotenv file.
--rm Delete the container on exit. Use for one-shots.
--restart=always Restart on exit. Boot survival also needs podman-restart.service enabled + loginctl enable-linger (rootless).
--user 1001:1001 Run the entrypoint as UID:GID (overrides Containerfile USER).
--cap-drop=ALL Drop every Linux capability.
--cap-add=NET_BIND_SERVICE Add specific ones back.
--read-only Mount the rootfs read-only; combine with explicit volumes/tmpfs.
--security-opt label=disable Disable SELinux confinement for this container.
--health-cmd "..." Override the image's HEALTHCHECK.

8. Networking

Three network drivers cover ~99% of cases:

podman network create app-net                          # bridge (default driver)
podman network create --driver host app-host           # host (no isolation)
podman network create --driver macvlan --subnet 192.168.1.0/24 -o parent=eth0 app-lan

podman network ls
podman network inspect app-net
podman network rm app-net

Use a named bridge network for anything multi-container. Containers on the same named network can resolve each other by name; on the default podman network, they can't.

# Postgres + app on the same named network; app uses 'postgres' as hostname.
podman network create demo-net

podman run -d --name postgres \
  --network demo-net \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  docker.io/library/postgres:16-alpine

podman run -d --name api \
  --network demo-net \
  -e DATABASE_URL=postgres://postgres:secret@postgres:5432/postgres \
  -p 8080:3000 \
  demo-app:1.0

Inside api, getent hosts postgres resolves to the postgres container's IP. No --link (that's a deprecated Docker thing); the named network gives you DNS for free.

Port mapping in rootless. -p 8080:80 works because 8080 is unprivileged. -p 80:80 fails because rootless can't bind < 1024 without help. Three fixes:

flowchart TD
    A["podman run -p 80:80<br/>rootless — unprivileged UID"] --> B{"Host port under 1024?"}
    B -- "no, e.g. 8080" --> D["Bind succeeds — no help needed"]
    B -- "yes — blocked by default" --> C["Pick a fix"]
    C --> E["1. Bump the host port<br/>-p 8080:80 (recommended)"]
    C --> F["2. Lower net.ipv4.ip_unprivileged_port_start<br/>system-wide sysctl"]
    C --> G["3. Grant CAP_NET_BIND_SERVICE<br/>to rootlesskit"]
# Option 1: bump the host port. Recommended.
podman run -p 8080:80 ...

# Option 2: lower the unprivileged-port boundary system-wide.
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/80-rootless.conf

# Option 3: grant the capability to rootlesskit (advanced).
sudo setcap cap_net_bind_service=ep $(which rootlesskit)

Rootless networking backend. Modern Podman uses pasta (faster, better IPv6) by default; older setups use slirp4netns. podman info | grep -i network shows which.


9. Storage — volumes and bind mounts

Three flavours, picked by syntax:

# Named volume — Podman tracks it.
podman volume create pgdata
podman run -v pgdata:/var/lib/postgresql/data postgres:16-alpine

# Bind mount — direct host path. The :Z suffix relabels for SELinux (private).
podman run -v /opt/app/data:/data:Z myimage

# Bind mount, shared SELinux label (multiple containers, same data).
podman run -v /opt/shared:/data:z myimage

# tmpfs — RAM-backed, ephemeral.
podman run --tmpfs /tmp:size=64m,mode=1777 myimage

# Read-only bind mount.
podman run -v /etc/resolv.conf:/etc/resolv.conf:ro myimage

Volume management:

podman volume ls
podman volume inspect pgdata
podman volume rm pgdata
podman volume prune                                    # delete unused

Two SELinux gotchas on RHEL:

  • :Z relabels the host path to container_file_t. Persistent. Once relabeled, every container can read/write. Forget :Z and the container gets EACCES even though chmod 777 looks fine — SELinux is denying it.
  • :z is shared label. Use when multiple containers need the same files. Don't use on /etc/somefile — you'd allow every container to touch that file.

Named volumes are SELinux-safe by default — Podman creates them with the right label.


10. Pods — the Kubernetes primitive, locally

A pod is a group of containers sharing network + IPC namespaces (and optionally PID + UTS). Same model Kubernetes uses; Podman gives you it on one host.

podman pod create --name webapp -p 8080:80 -p 8443:443
podman run -d --pod webapp --name nginx docker.io/library/nginx:1.25
podman run -d --pod webapp --name app demo-app:1.0
podman run -d --pod webapp --name cache docker.io/library/redis:7-alpine

podman pod ps
podman pod inspect webapp
podman pod stop webapp
podman pod rm -f webapp

What's true inside the pod:

  • All containers share localhostapp reaches redis via localhost:6379 and nginx via localhost:80. No DNS round-trip.
  • All ports must be published on the pod, not the container — that's what -p on podman pod create does.
  • Stop the pod, every container stops together. Remove the pod, every container is removed.

Pods + Quadlet are the canonical way to ship a small multi-container app on a single RHEL host without Kubernetes.


11. Security defaults you should know

By default, a Podman container:

  • Runs in a user namespace (rootless mode). The container's "root" is some unprivileged host UID.
  • Drops ~28 capabilities the kernel exposes. Only the safe subset is left.
  • Runs under a default seccomp profile that blocks ~50 dangerous syscalls.
  • Runs under an SELinux process label (container_t) that only allows access to container-labeled files.
  • Has its /proc and /sys partially masked so the container can't read sensitive host info.

You can tune (or disable) any of these:

# Drop everything, add only what's needed.
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage

# Custom seccomp profile.
podman run --security-opt seccomp=./profile.json myimage

# Disable SELinux confinement (last resort; document why).
podman run --security-opt label=disable myimage

# Read-only rootfs + tmpfs for writes.
podman run --read-only --tmpfs /tmp --tmpfs /var/run myimage

# Map container UID/GID 1:1 to host UID/GID (so file ownership matches).
podman run --userns=keep-id -v $PWD:/work:Z myimage

--userns=keep-id is the rootless killer feature for dev workflows: files created inside the container are owned by your host UID, so git status doesn't show a wall of permission warnings.


12. Systemd integration — Quadlet (the pointer)

The deprecated way:

podman generate systemd --new --name nginx --files
# Writes container-nginx.service to cwd
sudo cp container-nginx.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now container-nginx

This still works in Podman 5.x but is deprecated. The canonical way is Quadlet — drop a small .container file under /etc/containers/systemd/ (rootful) or ~/.config/containers/systemd/ (rootless) and systemctl daemon-reload. Systemd's Quadlet generator turns the file into a unit at boot.

That's module 10, Quadlet. Cross-reference there for the full surface. The one-liner mental model:

podman generate systemd → handwritten unit files, deprecated. Quadlet → declarative .container files, generated at boot, current.

podman auto-update reads container labels and, for any container labeled io.containers.autoupdate=registry, checks for a newer image and restarts the unit. Trigger with the bundled timer:

sudo systemctl enable --now podman-auto-update.timer

Once a day, the timer fires; containers labeled for auto-update get the newest image automatically.


13. Kubernetes YAML — podman play kube

The single-host K8s lookalike. Takes a Pod/Deployment/Service YAML and runs it as Podman pods + containers.

# webapp.yaml
apiVersion: v1
kind: Pod
metadata:
  name: webapp
spec:
  containers:
    - name: nginx
      image: docker.io/library/nginx:1.25
      ports:
        - containerPort: 80
          hostPort: 8080
    - name: app
      image: demo-app:1.0
      env:
        - name: REDIS_URL
          value: redis://localhost:6379
    - name: cache
      image: docker.io/library/redis:7-alpine
podman play kube webapp.yaml
podman play kube --down webapp.yaml         # tear down everything from this file
podman kube play webapp.yaml                # same command, newer subcommand name
podman generate kube webapp > webapp-export.yaml    # reverse direction

Use cases:

  • Local dev that mirrors prod K8s structure. Author the YAML once; run it on your laptop with Podman, deploy the same YAML to a real cluster.
  • Edge / single-host deployments. A small Pod + ConfigMap + Secret is enough; no need for the cluster.
  • CI smoke tests. Spin up the YAML in CI without a kind/minikube cluster.

Not a Kubernetes replacement; no scheduler, no rolling updates, no replicas across nodes.


14. Docker-to-Podman migration map

Docker Podman
docker run/ps/logs/exec/rm/... Same CLI surface; alias docker=podman works for ~95%.
dockerd daemon No daemon. conmon per container.
Container as root Container as unprivileged user via user namespaces. --userns=host to keep Docker-style behavior.
/var/run/docker.sock systemctl --user enable --now podman.socket$XDG_RUNTIME_DIR/podman/podman.sock. Set DOCKER_HOST so Docker SDKs work.
docker compose up podman compose up (built-in shim) or podman-compose (separate Python tool).
docker build podman build (calls Buildah under the hood). Same Dockerfile/Containerfile.
Bind mounts Add :Z (private) or :z (shared) for SELinux on RHEL.
-p 80:80 Rootless: fails. Use 8080:80, or bump net.ipv4.ip_unprivileged_port_start, or grant capability.
--link (deprecated) Use a named bridge network; DNS-by-name works automatically.
docker network create podman network create. Same drivers.
docker volume create podman volume create. Same model.
docker swarm (deprecated) Choose Kubernetes (k3s for tiny).
Restart policy via daemon Restart policy + Quadlet for boot-time supervision.

The 80/20 of "we switched from Docker to Podman":

  1. Replace docker with podman in scripts.
  2. Add :Z to bind mounts on RHEL.
  3. Move boot-time container management from --restart to Quadlet.
  4. Decide rootless vs rootful per host (rootless is the default; rootful for things that genuinely need it like --network host with privileged ports, certain CNI plugins).

15. Worked example — Postgres + Node API + Caddy

End-to-end on a single RHEL host, all rootless, all on a private bridge network, with healthchecks and SELinux relabel.

# 0. Network + volumes
podman network create demo-net
podman volume create pgdata
podman volume create caddydata
podman volume create caddyconfig

# 1. Postgres
podman run -d --name postgres \
  --network demo-net \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=demo \
  --health-cmd "pg_isready -U postgres -d demo" \
  --health-interval 10s \
  --health-retries 5 \
  docker.io/library/postgres:16-alpine

# Wait for healthy
until podman healthcheck run postgres; do sleep 1; done

# 2. App
podman run -d --name api \
  --network demo-net \
  -e DATABASE_URL=postgres://postgres:secret@postgres:5432/demo \
  -e PORT=3000 \
  --health-cmd "wget -qO- http://localhost:3000/healthz || exit 1" \
  --health-interval 30s \
  demo-app:1.0

# 3. Caddy reverse proxy
mkdir -p ./caddy
cat > ./caddy/Caddyfile <<'EOF'
:80 {
  reverse_proxy api:3000
}
EOF

podman run -d --name caddy \
  --network demo-net \
  -v ./caddy/Caddyfile:/etc/caddy/Caddyfile:Z \
  -v caddydata:/data \
  -v caddyconfig:/config \
  -p 8080:80 \
  docker.io/library/caddy:2-alpine

# 4. Verify
podman ps
podman logs caddy
curl -i http://localhost:8080/
curl -i http://localhost:8080/healthz

# 5. Inspect a container's healthcheck history
podman inspect api --format '{{json .State.Healthcheck}}' | jq .

# 6. Iterate on app
podman pull myregistry.example.com/demo-app:1.1
podman stop api && podman rm api
podman run -d --name api \
  --network demo-net \
  -e DATABASE_URL=postgres://postgres:secret@postgres:5432/demo \
  -e PORT=3000 \
  myregistry.example.com/demo-app:1.1

# 7. Teardown
podman stop caddy api postgres
podman rm caddy api postgres
podman volume rm pgdata caddydata caddyconfig
podman network rm demo-net

For production use, every podman run above becomes a Quadlet .container file (see module 10, Quadlet), and the volumes/network become .volume/.network files. The hand-typed orchestration above is the development/learning loop.


16. What's intentionally out of scope here

Topic Pointer when you need it
Quadlet (.container etc.) module 10, Quadlet. The canonical systemd integration.
Buildah deep dive man buildah. Podman delegates build to it; you only touch Buildah directly for advanced builds.
Skopeo deep dive man skopeo. Copy/inspect images without pulling; useful in air-gapped + supply-chain workflows.
Kubernetes-on-Podman Real K8s. podman play kube is a single-host runner, not a control plane.
Custom storage drivers containers-storage.conf(5). The overlay default is right for ~99% of cases.
The Podman REST API socket man podman-system-service. Run it when a Docker-SDK tool needs to talk to Podman.
Podman Desktop (GUI, Windows) podman-desktop.io. Helpful on workstations; not in the RHEL server workflow.
Network plugins (CNI / Netavark) Default is Netavark on modern Podman; CNI is being phased out. man podman-network.
Custom seccomp / SELinux policies podman run --security-opt. Reach for it when an audit demands it; default profile is sane.
CRI-O / OCI runtime swap crun (default on RHEL 9) vs runc. Change in containers.conf; default is right for ~99%.

17. Reference card — daily commands

# Setup
sudo dnf install -y podman                                # RHEL/Fedora
podman info                                               # engine + storage info
podman version

# Images
podman pull <registry>/<image>:<tag>
podman images
podman build -t name:tag -f Containerfile .
podman tag src:tag dst:tag
podman push <registry>/<image>:<tag>
podman login <registry>
podman save -o file.tar image:tag
podman load -i file.tar
podman rmi image:tag
podman manifest create/build/push                         # multi-arch

# Containers
podman run -d --name N --network net -p H:C -v V:/p:Z image
podman ps           ; podman ps -a
podman logs -f N    ; podman logs --tail 50 N
podman exec -it N /bin/sh
podman inspect N
podman stop N       ; podman start N      ; podman restart N
podman rm N         ; podman rm -f N
podman cp src dst:/path
podman top N        ; podman stats N
podman healthcheck run N

# Networks
podman network ls
podman network create my-net
podman network inspect my-net
podman network rm my-net

# Volumes
podman volume ls
podman volume create vol
podman volume inspect vol
podman volume rm vol
podman volume prune

# Pods
podman pod create --name webapp -p 8080:80
podman run --pod webapp ...
podman pod ps
podman pod inspect webapp
podman pod stop webapp
podman pod rm webapp

# Kubernetes YAML
podman play kube webapp.yaml
podman kube play --down webapp.yaml
podman generate kube webapp > webapp.yaml

# System / maintenance
podman info
podman system df
podman system prune
podman system reset                                       # destructive
podman events                                             # real-time event stream
podman --log-level debug ...                              # verbose

# Migration / compat
alias docker=podman
systemctl --user enable --now podman.socket               # Docker-API compat
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"

That's the 80%. Go run podman run --rm -it docker.io/library/alpine:3.19 sh and see how it feels.


18. How this connects

  • RHEL's SELinux & users page (module 09, Linux (RHEL), 04-selinux-and-users.md §2) — the Multi-Category Security mechanism that actually makes §9's :Z (private) vs :z (shared) relabel work: every container gets the same container_t type, but :Z assigns a fresh, isolated category pair while :z joins a shared one — same type, different category, denied by default.
  • Quadlet (module 10, Quadlet, course.md + 01-container-unit.md) — the systemd-native replacement for §12's deprecated podman generate systemd; both quadlet pages already point back here for the underlying conmon model and the podman run/build vocabulary their [Container] keys translate into.
  • Kubernetes Workloads (module 04, Kubernetes & Helm, 01-workloads.md §1) — the same Pod primitive §10 gives you on one host: a group of containers on one node sharing a network namespace and storage volumes. §13's podman play kube runs that same Pod/Deployment/Service object model directly, without a cluster.