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 ✓
FeatureDockerPodman
DaemonRequires dockerd (root)Daemonless
RootlessPossible but not defaultDefault mode
CLI compatibilityN/A (it IS Docker)Nearly identical
Pod supportNo native podsFirst-class pods
systemd integrationLimitedpodman generate systemd
Image formatOCI + Docker v2OCI + Docker v2
ComposeDocker Composepodman-compose or podman compose
Default runtimerunccrun (faster, written in C)
Default onUbuntu, most cloudFedora, 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 .container files 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

  1. Rootless basics: Run an nginx container with Podman as your regular user. Verify with ps aux that the container processes run as your user, not root. Access the web server to confirm it works.

  2. 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.

  3. 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.

  4. 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.

  5. Skopeo exploration: Use skopeo inspect to examine the python:3.12-slim image on Docker Hub without downloading it. Find out the image size, creation date, and architecture.

  6. Bonus Challenge: Set up a rootless Podman environment on a system where Docker is also installed. Configure the Podman socket and set DOCKER_HOST so that docker-compose (the Docker tool) actually uses Podman as its backend. This demonstrates the socket compatibility layer.