Podman
Why This Matters
Docker changed the world, but it has a design choice that makes security-conscious sysadmins uneasy: the Docker daemon. Every Docker command talks to dockerd, a long-running daemon that runs as root. If that daemon is compromised, an attacker has root on your host. If the daemon crashes, every container on the system goes down with it.
Podman was created by Red Hat to address these concerns. It is daemonless (no central service to crash or compromise), rootless (containers can run without any root privileges), and CLI-compatible with Docker (you can literally alias docker=podman and most workflows continue to work). Podman also introduces the concept of pods -- groups of containers that share namespaces -- which mirrors Kubernetes pod architecture.
If you are on RHEL, CentOS Stream, Fedora, or any environment that values security, Podman is likely your default container runtime. Understanding Podman means understanding the future direction of Linux containerization.
Try This Right Now
If you are on Fedora or RHEL 8+, Podman is probably already installed:
$ podman --version
podman version 4.9.4
$ podman run --rm docker.io/library/hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
...
Notice that the hello-world image comes from Docker Hub (docker.io/library/). Podman can pull from the same registries as Docker.
Now check whether you are running rootless:
$ podman info --format '{{.Host.Security.Rootless}}'
true
If it says true, you are running containers without any root privileges. That is Podman's default behavior when run as a regular user.
Podman vs Docker: Key Differences
Docker Architecture: Podman Architecture:
┌────────┐ ┌──────────┐ ┌────────┐
│docker │────►│ dockerd │ │podman │
│CLI │ │ (daemon) │ │CLI │
└────────┘ │ (root) │ └───┬────┘
└────┬─────┘ │
│ │ (direct fork/exec)
▼ ▼
┌──────────┐ ┌──────────┐
│containerd│ │ conmon │
└────┬─────┘ └────┬─────┘
▼ ▼
┌──────────┐ ┌──────────┐
│ runc │ │ crun │
└──────────┘ └──────────┘
Single point of failure ✗ No daemon ✓
Requires root daemon ✗ Rootless by default ✓
Daemon crash kills all ✗ Process-per-container ✓
| Feature | Docker | Podman |
|---|---|---|
| Daemon | Requires dockerd (root) | Daemonless |
| Rootless | Possible but not default | Default mode |
| CLI compatibility | N/A (it IS Docker) | Nearly identical |
| Pod support | No native pods | First-class pods |
| systemd integration | Limited | podman generate systemd |
| Image format | OCI + Docker v2 | OCI + Docker v2 |
| Compose | Docker Compose | podman-compose or podman compose |
| Default runtime | runc | crun (faster, written in C) |
| Default on | Ubuntu, most cloud | Fedora, RHEL, CentOS |
Installing Podman
Fedora/RHEL/CentOS
# Usually pre-installed, but if not:
$ sudo dnf install -y podman
Debian/Ubuntu
$ sudo apt update
$ sudo apt install -y podman
Arch Linux
$ sudo pacman -S podman
Distro Note: On older Ubuntu (20.04), the packaged Podman version may be outdated. Use the Kubic project repository for a newer version. On Ubuntu 22.04+, the version in the default repos is usually adequate.
Verify installation:
$ podman --version
$ podman info
Rootless Setup
Podman runs rootless out of the box, but it needs subordinate UID/GID ranges configured:
# Check that your user has subuid/subgid entries
$ grep $USER /etc/subuid
user:100000:65536
$ grep $USER /etc/subgid
user:100000:65536
If those entries are missing:
$ sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
These subordinate UIDs allow user namespace mapping -- the mechanism that lets a rootless container have a "root" user (UID 0) that maps to an unprivileged UID on the host.
Using Podman: The Familiar CLI
If you know Docker, you know Podman. The commands are nearly identical.
Running Containers
# Run an interactive container
$ podman run -it docker.io/library/ubuntu:22.04 bash
# Run a container in the background
$ podman run -d --name my-nginx -p 8080:80 docker.io/library/nginx
# Test it
$ curl http://localhost:8080
# View logs
$ podman logs my-nginx
$ podman logs -f my-nginx
# Execute commands in a running container
$ podman exec -it my-nginx bash
# Stop and remove
$ podman stop my-nginx
$ podman rm my-nginx
# List containers
$ podman ps -a
# List images
$ podman images
Registry Configuration
Podman requires fully qualified image names by default (unlike Docker which assumes docker.io/library/). You can configure search registries:
# Pull with full path
$ podman pull docker.io/library/nginx:latest
# Or configure default search registries
$ cat /etc/containers/registries.conf
unqualified-search-registries = ["docker.io", "quay.io"]
With that configuration:
# Now short names work
$ podman pull nginx:latest
Think About It: Podman requiring fully qualified image names is a security feature. When you type
docker pull python, how do you know which registry that comes from? Is it Docker Hub? A compromised mirror? Podman forces you to be explicit:docker.io/library/python. This prevents accidental pulls from unexpected sources.
Rootless Containers in Detail
When you run podman run as a regular user, here is what happens:
Host System: Container:
┌──────────────────────────┐ ┌──────────────────────┐
│ Your user (UID 1000) │ │ root (UID 0) │
│ │ maps │ │
│ Subordinate UIDs: │ -------> │ Container users: │
│ 100000 → UID 1 in │ │ UID 1 │
│ 100001 → UID 2 in │ │ UID 2 │
│ ... │ │ ... │
│ 165535 → UID 65536 │ │ UID 65536 │
│ │ │ │
│ Your user owns the │ │ "root" inside has │
│ container process │ │ no host root privs │
└──────────────────────────┘ └──────────────────────┘
# Run a rootless container
$ podman run -d --name rootless-test docker.io/library/nginx
# Check the process on the host
$ ps aux | grep nginx
user 12345 0.0 0.1 ... nginx: master process
user 12346 0.0 0.1 ... nginx: worker process
# The nginx processes run as YOUR user, not root!
# Inside the container, it appears to be root
$ podman exec rootless-test id
uid=0(root) gid=0(root) groups=0(root)
# But the UID mapping shows the truth
$ podman exec rootless-test cat /proc/1/uid_map
0 1000 1
1 100000 65536
Rootless containers have some limitations:
- Cannot bind to ports below 1024 (by default)
- Cannot use some network features that require root
- Storage performance may differ slightly
# Binding to low ports as rootless user:
$ podman run -p 80:80 nginx
Error: rootlessport cannot expose privileged port 80
# Fix: use a high port
$ podman run -p 8080:80 nginx
# Or allow low port binding system-wide (requires root once)
$ sudo sysctl net.ipv4.ip_unprivileged_port_start=80
Pods: Podman's Unique Feature
A pod is a group of containers that share the same network namespace, PID namespace, and IPC namespace. This directly mirrors the Kubernetes pod concept.
┌─────────────────────────────────────────┐
│ Pod │
│ ┌──────────┐ ┌──────────┐ │
│ │ Web App │ │ Sidecar │ │
│ │ Container│ │ Container│ │
│ │ port 8000│ │ port 9090│ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ─────┴──────────────┴──────── │
│ Shared network namespace │
│ (containers see each other │
│ on localhost) │
│ │
│ Infra container (pause) │
│ Holds namespaces open │
└─────────────────────────────────────────┘
Hands-On: Working with Pods
# Create a pod with port mapping
$ podman pod create --name my-pod -p 8080:80
# List pods
$ podman pod list
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
a1b2c3d4e5f6 my-pod Created 5 seconds ago f6e5d4c3b2a1 1
# Add an nginx container to the pod
$ podman run -d --pod my-pod --name web docker.io/library/nginx
# Add a sidecar container to the same pod
$ podman run -d --pod my-pod --name sidecar docker.io/library/alpine \
sh -c "while true; do wget -qO- http://localhost:80 && sleep 5; done"
# The sidecar can reach nginx on localhost because they share a network namespace!
# View pod details
$ podman pod inspect my-pod
# View containers in the pod
$ podman ps --pod
CONTAINER ID IMAGE COMMAND STATUS PORTS POD ID PODNAME
f6e5d4c3b2a1 localhost/podman-pause:4.9.4-0 /pause Up 2 minutes 0.0.0.0:8080->80/tcp a1b2c3d4e5f6 my-pod
9a8b7c6d5e4f docker.io/library/nginx:latest nginx -g daemon o... Up 2 minutes 0.0.0.0:8080->80/tcp a1b2c3d4e5f6 my-pod
1a2b3c4d5e6f docker.io/library/alpine:latest sh -c while true... Up 1 minute 0.0.0.0:8080->80/tcp a1b2c3d4e5f6 my-pod
# Stop the entire pod
$ podman pod stop my-pod
# Remove the pod and all its containers
$ podman pod rm my-pod
Think About It: The pod model is powerful because related containers (app + log forwarder, app + metrics collector, app + TLS proxy) can communicate over localhost, just as if they were processes on the same machine. This is the design pattern Kubernetes uses for sidecar containers.
Generating systemd Units
One of Podman's killer features for production use is generating systemd service files directly from containers. This lets systemd manage your containers like any other service -- starting them at boot, restarting on failure, and managing dependencies.
# Run a container
$ podman run -d --name my-web -p 8080:80 docker.io/library/nginx
# Generate a systemd unit file
$ podman generate systemd --name my-web --new --files
/home/user/container-my-web.service
# View the generated file
$ cat container-my-web.service
[Unit]
Description=Podman container-my-web.service
Wants=network-online.target
After=network-online.target
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run \
--cidfile=%t/%n.ctr-id \
--cgroups=no-conmon \
--rm \
--sdnotify=conmon \
-d \
--replace \
--name my-web \
-p 8080:80 \
docker.io/library/nginx
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
Install and enable:
# For rootless containers (user-level systemd)
$ mkdir -p ~/.config/systemd/user/
$ cp container-my-web.service ~/.config/systemd/user/
$ systemctl --user daemon-reload
$ systemctl --user enable --now container-my-web.service
$ systemctl --user status container-my-web.service
# For system-level containers (requires root)
$ sudo cp container-my-web.service /etc/systemd/system/
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now container-my-web.service
For user-level services to run at boot (even before login):
$ sudo loginctl enable-linger $USER
Distro Note: On newer Podman versions (4.4+), Podman recommends Quadlet files instead of
podman generate systemd. Quadlet uses.containerfiles in~/.config/containers/systemd/that are simpler to write and maintain.
Quadlet (Modern Approach)
Create ~/.config/containers/systemd/my-web.container:
[Container]
Image=docker.io/library/nginx
PublishPort=8080:80
[Service]
Restart=always
[Install]
WantedBy=default.target
$ systemctl --user daemon-reload
$ systemctl --user start my-web
$ systemctl --user status my-web
Buildah: Building Images Without a Daemon
Buildah is a companion tool for building OCI container images. While Podman can also build images (using podman build), Buildah offers more flexibility and does not require a Dockerfile.
# Install Buildah
$ sudo dnf install -y buildah # Fedora/RHEL
$ sudo apt install -y buildah # Debian/Ubuntu
Building with a Dockerfile
# Buildah can build from Dockerfiles
$ buildah build -t my-app -f Dockerfile .
# This is equivalent to
$ podman build -t my-app -f Dockerfile .
Building Without a Dockerfile
Buildah's unique feature is scripted builds:
# Create a container from a base image
$ container=$(buildah from docker.io/library/ubuntu:22.04)
# Run commands inside it
$ buildah run $container -- apt-get update
$ buildah run $container -- apt-get install -y python3
$ buildah run $container -- mkdir /app
# Copy files in
$ buildah copy $container app.py /app/app.py
# Set configuration
$ buildah config --cmd "python3 /app/app.py" $container
$ buildah config --port 8000 $container
$ buildah config --author "Your Name" $container
# Commit as a new image
$ buildah commit $container my-scripted-app
# Clean up the working container
$ buildah rm $container
# The image is now available
$ podman images | grep my-scripted-app
Why use Buildah over a Dockerfile?
- Shell scripting (loops, conditionals, variables)
- No daemon required
- Fine-grained layer control
- Can mount host directories during build without COPY
Skopeo: Inspecting and Copying Images
Skopeo inspects and copies container images between registries without pulling them to local storage first.
# Install Skopeo
$ sudo dnf install -y skopeo # Fedora/RHEL
$ sudo apt install -y skopeo # Debian/Ubuntu
# Inspect a remote image without downloading it
$ skopeo inspect docker://docker.io/library/nginx:latest
{
"Name": "docker.io/library/nginx",
"Tag": "latest",
"Digest": "sha256:...",
"Created": "2024-01-15T...",
"Architecture": "amd64",
"Os": "linux",
...
}
# Copy an image between registries (no local pull needed)
$ skopeo copy docker://docker.io/library/nginx:latest \
docker://registry.example.com/nginx:latest
# Copy an image to a local directory (for air-gapped environments)
$ skopeo copy docker://docker.io/library/nginx:latest \
dir:/tmp/nginx-image
# Copy an image to a local OCI archive
$ skopeo copy docker://docker.io/library/nginx:latest \
oci-archive:/tmp/nginx.tar
# List tags for a remote image
$ skopeo list-tags docker://docker.io/library/nginx
Skopeo is invaluable for:
- Inspecting images before pulling them
- Copying images between registries
- Mirroring images for air-gapped environments
- Checking image digests and signatures
Podman Compose
Podman supports Docker Compose files through two methods:
Method 1: podman-compose (standalone tool)
$ sudo dnf install -y podman-compose # Fedora
$ pip install podman-compose # Any distro
# Uses the same docker-compose.yml files
$ podman-compose up -d
$ podman-compose ps
$ podman-compose down
Method 2: podman compose (built-in, uses docker-compose or podman-compose)
# Podman 4.1+ can use the compose subcommand
$ podman compose up -d
Both methods work with standard docker-compose.yml files. Here is an example:
# docker-compose.yml (works with both Docker and Podman)
services:
web:
image: docker.io/library/nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
Migrating from Docker to Podman
If you are coming from Docker, the migration is straightforward.
Step 1: The alias trick
# Add to ~/.bashrc
alias docker=podman
Most Docker commands work identically with Podman. The exceptions are Docker-specific features like docker swarm.
Step 2: Update image references
# Docker (implicit docker.io)
$ docker pull nginx
# Podman (explicit registry recommended)
$ podman pull docker.io/library/nginx
Step 3: Replace Docker socket for tools that need it
Some tools expect /var/run/docker.sock. Podman can emulate this:
# Enable the Podman socket (rootless)
$ systemctl --user enable --now podman.socket
# The socket is at
$ ls $XDG_RUNTIME_DIR/podman/podman.sock
# Set DOCKER_HOST for compatibility
$ export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
Step 4: Handle compose files
Replace docker compose with podman-compose or podman compose.
Step 5: Replace systemd integration
Instead of Docker's --restart=always, use podman generate systemd or Quadlet files for proper systemd integration.
Debug This
A user tries to run a rootless container but gets a permission error:
$ podman run -d -p 8080:80 docker.io/library/nginx
Error: error creating container storage: ... operation not permitted
Diagnosis:
# Check subuid/subgid configuration
$ grep $USER /etc/subuid
# (empty -- no output)
$ grep $USER /etc/subgid
# (empty -- no output)
The problem: The user does not have subordinate UID/GID ranges configured. Rootless containers need these for user namespace mapping.
Fix:
$ sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
# Reset Podman's user storage
$ podman system migrate
# Try again
$ podman run -d -p 8080:80 docker.io/library/nginx
# Works!
What Just Happened?
┌─────────────────────────────────────────────────────────────┐
│ CHAPTER RECAP │
├─────────────────────────────────────────────────────────────┤
│ │
│ Podman is a daemonless, rootless container engine. │
│ │
│ No daemon = no single point of failure, smaller │
│ attack surface, no root daemon process. │
│ │
│ Rootless by default = containers run as your user, │
│ UID 0 inside maps to unprivileged UID on host. │
│ │
│ CLI is nearly identical to Docker. alias docker=podman │
│ works for most workflows. │
│ │
│ Pods group containers sharing network/IPC namespaces, │
│ mirroring Kubernetes pod architecture. │
│ │
│ podman generate systemd / Quadlet files integrate │
│ containers with systemd for production use. │
│ │
│ Buildah builds images without a daemon. │
│ Skopeo inspects and copies images between registries. │
│ │
└─────────────────────────────────────────────────────────────┘
Try This
-
Rootless basics: Run an nginx container with Podman as your regular user. Verify with
ps auxthat the container processes run as your user, not root. Access the web server to confirm it works. -
Pod creation: Create a pod with two containers: an nginx web server and an Alpine container that periodically curls
http://localhost:80. Verify the sidecar container can reach nginx via localhost. -
systemd integration: Generate a systemd unit file for a container with
podman generate systemd. Install it as a user-level service. Reboot and verify the container starts automatically. -
Buildah scripted build: Use Buildah commands (not a Dockerfile) to create an image that contains your favorite programming language runtime and a simple script. Commit it and run it with Podman.
-
Skopeo exploration: Use
skopeo inspectto examine thepython:3.12-slimimage on Docker Hub without downloading it. Find out the image size, creation date, and architecture. -
Bonus Challenge: Set up a rootless Podman environment on a system where Docker is also installed. Configure the Podman socket and set
DOCKER_HOSTso thatdocker-compose(the Docker tool) actually uses Podman as its backend. This demonstrates the socket compatibility layer.