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 (
conmonper container, nodockerd), rootless by default, and systemd-first —alias docker=podmancovers ~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.containerunit (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
podmaninvocation is a self-contained process. Nodockerd, 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 dockerafter host upgrades. Nothing to restart. - Drop-in CLI.
alias docker=podmanworks for ~95% of commands you've muscle-memorised. - Native pods.
podman pod creategives 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 serviceto 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:
- No daemon.
podman runforksconmon(a tiny per-container monitor) and the container process, thenpodmanexits.conmonkeeps the container running and reports back to whoever cares (your shell, or systemd via Quadlet). - 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. - State is per-user. Rootless storage lives in
~/.local/share/containers/; rootful in/var/lib/containers/.podman psas 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 justalpine. 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:80is 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), setsysctl net.ipv4.ip_unprivileged_port_start=80, or grantCAP_NET_BIND_SERVICEtorootlesskit. podman psonly 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:
USERmatters. RHEL UBI images run as UID 1001 by default. Files copied from the build stage are owned by root unless you--chown=1001:1001orchownafter copying. Permission-denied at runtime almost always traces back to this.- Cache busts on COPY.
COPY . .followed byRUN npm cire-installs deps every time any file changes. Two-step COPY (package*.jsonfirst, thennpm 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:
:Zrelabels the host path tocontainer_file_t. Persistent. Once relabeled, every container can read/write. Forget:Zand the container gets EACCES even thoughchmod 777looks fine — SELinux is denying it.:zis 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
localhost—appreachesredisvialocalhost:6379andnginxvialocalhost:80. No DNS round-trip. - All ports must be published on the pod, not the container — that's what
-ponpodman pod createdoes. - 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
/procand/syspartially 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.containerfiles, 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":
- Replace
dockerwithpodmanin scripts. - Add
:Zto bind mounts on RHEL. - Move boot-time container management from
--restartto Quadlet. - Decide rootless vs rootful per host (rootless is the default; rootful for things that genuinely need it like
--network hostwith 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 samecontainer_ttype, but:Zassigns a fresh, isolated category pair while:zjoins 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 deprecatedpodman generate systemd; both quadlet pages already point back here for the underlyingconmonmodel and thepodman run/buildvocabulary 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'spodman play kuberuns that same Pod/Deployment/Service object model directly, without a cluster.