Files
howtos/topics/podman.md
user 42cf66377e docs: add podman howto
Covers images, containers, Containerfile, build, volumes, networks,
pods, podman-compose, systemd integration, and Quadlet files.
2026-02-21 20:53:37 +01:00

9.8 KiB

Podman

Daemonless container engine — Docker-compatible, rootless by default.

Core Concepts

Concept Description
Image Read-only template for creating containers
Container Running (or stopped) instance of an image
Pod Group of containers sharing network/IPC namespace
Volume Persistent storage decoupled from container life
Registry Remote image store (docker.io, ghcr.io, quay.io)
Containerfile Build instructions (alias: Dockerfile)

Images

# Search
podman search nginx
podman search --list-tags docker.io/library/python

# Pull
podman pull nginx
podman pull docker.io/library/python:3.12-slim

# List
podman images
podman images --format "{{.Repository}}:{{.Tag}}  {{.Size}}"

# Inspect
podman inspect nginx:latest
podman image inspect nginx:latest --format '{{.Config.ExposedPorts}}'

# Remove
podman rmi nginx:latest
podman rmi -f <image-id>               # force
podman image prune                     # remove dangling
podman image prune -a                  # remove all unused

Containers

# Run
podman run nginx                       # foreground
podman run -d nginx                    # detached
podman run --rm nginx cat /etc/os-release  # run + remove after exit
podman run -it ubuntu bash             # interactive terminal

# Common run flags
podman run -d \
  --name myapp \
  -p 8080:80 \                         # host:container port
  -v ./data:/app/data:Z \              # bind mount (:Z for SELinux)
  -e APP_ENV=production \              # environment variable
  --env-file .env \                    # env from file
  -w /app \                            # working directory
  --restart unless-stopped \           # restart policy
  --memory 512m \                      # memory limit
  --cpus 1.5 \                         # CPU limit
  myimage:latest

# List
podman ps                              # running
podman ps -a                           # all (including stopped)
podman ps --format "{{.Names}}  {{.Status}}  {{.Ports}}"

# Lifecycle
podman start myapp
podman stop myapp
podman stop -t 5 myapp                 # 5s grace period
podman restart myapp
podman kill myapp                      # SIGKILL
podman rm myapp
podman rm -f myapp                     # force (stop + remove)

# Exec into running container
podman exec -it myapp bash
podman exec myapp cat /etc/hostname

# Logs
podman logs myapp
podman logs -f myapp                   # follow
podman logs --tail 50 myapp            # last 50 lines
podman logs --since 1h myapp           # last hour

# Copy files
podman cp myapp:/app/log.txt ./log.txt
podman cp ./config.yml myapp:/app/config.yml

# Stats
podman stats                           # all containers
podman stats myapp                     # specific

# Inspect
podman inspect myapp
podman inspect myapp --format '{{.NetworkSettings.IPAddress}}'

# Cleanup
podman container prune                 # remove all stopped

Port Mapping

-p 8080:80                             # host 8080 -> container 80
-p 127.0.0.1:8080:80                  # bind to localhost only
-p 8080:80/udp                         # UDP
-p 8080-8090:80-90                     # port range
--network host                         # use host network directly

Restart Policies

Policy Behavior
no Never restart (default)
on-failure[:N] Restart on non-zero exit [max N times]
always Always restart, including on boot
unless-stopped Like always, but not if manually stopped

Building Images

Containerfile

FROM python:3.12-slim

ARG APP_VERSION=1.0.0
ENV APP_ENV=production

WORKDIR /app

# Dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Application code
COPY src/ ./src/

EXPOSE 8080

RUN useradd -r appuser
USER appuser

ENTRYPOINT ["python", "-m", "src.app"]
CMD ["--port", "8080"]

Build Commands

# Build
podman build -t myapp .
podman build -t myapp:v1.2 -f Containerfile .

# Build args
podman build --build-arg APP_VERSION=2.0 -t myapp .

# No cache
podman build --no-cache -t myapp .

# Multi-stage (use target)
podman build --target builder -t myapp-build .

# Build from stdin
cat Containerfile | podman build -t myapp -

# Tag
podman tag myapp:latest myapp:v1.2
podman tag myapp registry.example.com/myapp:v1.2

# Push to registry
podman login registry.example.com
podman push registry.example.com/myapp:v1.2

# Save / load (offline transfer)
podman save myapp:latest -o myapp.tar
podman load -i myapp.tar

# History (show layers)
podman history myapp:latest

Containerfile Best Practices

Practice Reason
Pin base image versions Reproducible builds
COPY deps before code Maximize layer cache
--no-cache-dir on pip Smaller image
Use -slim base images Smaller attack surface
USER nonroot Don't run as root
One RUN per logical step Readable + cacheable
.containerignore Exclude .git, node_modules, etc.

Volumes

# Create named volume
podman volume create mydata

# List / inspect
podman volume ls
podman volume inspect mydata

# Use in container
podman run -d -v mydata:/app/data myapp

# Bind mount (host directory)
podman run -d -v /host/path:/container/path:Z myapp
podman run -d -v ./local:/app/data:Z myapp     # relative path

# Read-only mount
podman run -d -v ./config:/app/config:ro,Z myapp

# Tmpfs (in-memory)
podman run -d --tmpfs /tmp:size=100m myapp

# Remove
podman volume rm mydata
podman volume prune                    # remove unused

Mount Flags

Flag Purpose
:Z Private SELinux label (single container)
:z Shared SELinux label (multiple containers)
:ro Read-only
:U Chown to container user (rootless permission fix)

Networks

# List
podman network ls

# Create
podman network create mynet
podman network create --subnet 10.89.0.0/24 mynet

# Run on network
podman run -d --network mynet --name web myapp
podman run -d --network mynet --name db postgres

# Containers on same network resolve by name
# web can reach db at hostname "db"

# Connect/disconnect running container
podman network connect mynet myapp
podman network disconnect mynet myapp

# Inspect
podman network inspect mynet

# Remove
podman network rm mynet
podman network prune

Pods

# Create pod (shared network namespace)
podman pod create --name mypod -p 8080:80

# Run containers in pod
podman run -d --pod mypod --name web nginx
podman run -d --pod mypod --name app myapp

# Containers in same pod share localhost

# Pod management
podman pod ls
podman pod start mypod
podman pod stop mypod
podman pod rm mypod
podman pod rm -f mypod                 # force

# Generate kube YAML from pod
podman generate kube mypod > pod.yml

# Create pod from kube YAML
podman play kube pod.yml
podman play kube --down pod.yml        # tear down

Podman Compose

# compose.yml
services:
  web:
    build: .
    ports:
      - "8080:80"
    volumes:
      - ./src:/app/src:Z
    environment:
      - APP_ENV=development
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp

volumes:
  pgdata:
podman-compose up -d
podman-compose down
podman-compose logs -f web
podman-compose ps
podman-compose exec web bash
podman-compose build
podman-compose pull

Systemd Integration

# Generate systemd unit from container
podman generate systemd --new --name myapp > ~/.config/systemd/user/myapp.service

# For rootless, enable lingering
loginctl enable-linger $USER

# Enable as user service
systemctl --user daemon-reload
systemctl --user enable --now myapp.service

# Quadlet (modern, preferred over generate systemd)
# Place .container file in ~/.config/containers/systemd/

Quadlet File

# ~/.config/containers/systemd/myapp.container
[Container]
Image=myapp:latest
PublishPort=8080:80
Volume=./data:/app/data:Z
Environment=APP_ENV=production

[Service]
Restart=on-failure

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user start myapp.service

Cleanup

# Remove stopped containers
podman container prune

# Remove unused images
podman image prune -a

# Remove unused volumes
podman volume prune

# Remove unused networks
podman network prune

# Nuclear option — everything unused
podman system prune -a --volumes

Gotchas

  • :Z/:z is required on SELinux systems (Fedora, RHEL) or mounts fail silently
  • Rootless containers can't bind to ports < 1024 — use higher ports or sysctl net.ipv4.ip_unprivileged_port_start
  • podman-compose is a separate package — not bundled with podman
  • Rootless volume mounts may have UID mapping issues — use :U flag or --userns=keep-id
  • podman generate systemd is deprecated — use Quadlet .container files instead
  • Container DNS resolution by name only works on user-created networks, not the default podman network
  • podman system prune does not remove named volumes unless --volumes is passed
  • --restart policies only work with podman start, not across reboots — use systemd for boot persistence

See Also