Podman

Podman rootless containers and systemd integration.

Podman Basics

# Podman is mostly docker-compatible
podman ps -a
podman images
podman run -d --name myapp nginx:alpine
podman stop myapp
podman rm myapp

# Pull image
podman pull docker.io/library/nginx:alpine

# Run with port mapping
podman run -d -p 8080:80 --name web nginx:alpine

# Interactive container
podman run -it --rm fedora:latest bash

KEY DIFFERENCE: Podman is daemonless - each container is a child process. No root daemon means no single point of failure.

Podman vs Docker Compatibility

# Make docker commands use podman
alias docker=podman

# Or use podman-docker package (provides /usr/bin/docker symlink)
sudo pacman -S podman-docker  # Arch
sudo dnf install podman-docker  # Fedora/RHEL

# Check compatibility
podman info --format '{{.Host.RemoteSocket.Path}}'

# Docker Compose with Podman
# Option 1: podman-compose (Python)
pip install podman-compose
podman-compose up -d

# Option 2: docker-compose with podman socket
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
docker compose up -d  # Now uses podman

GOTCHA: Some Docker features don’t work: - docker swarm (use Kubernetes instead) - --privileged may need --security-opt label=disable - Some network modes differ

Rootless Containers

# Check rootless status
podman info | grep -A5 "rootless:"

# Initial rootless setup (done automatically on first run)
podman system migrate

# View user namespace mappings
podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map

# Check subuid/subgid ranges
cat /etc/subuid | grep $(whoami)
cat /etc/subgid | grep $(whoami)

# If missing, add them
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
podman system migrate

# Run container as specific user inside
podman run --rm -it --user 1000:1000 fedora id

# Map host user to container root
podman run --rm -it --userns=keep-id fedora id
# Shows your UID inside container

SECURITY: Rootless means container root != host root. Even if container is compromised, attacker has unprivileged access only.

Image Management

# List images
podman images

# List with digests
podman images --digests

# Search registries
podman search nginx
podman search --filter=is-official nginx

# Pull from specific registry
podman pull docker.io/library/nginx:alpine
podman pull quay.io/prometheus/prometheus:latest
podman pull ghcr.io/owner/image:tag

# Build image
podman build -t myapp:latest .
podman build -t myapp:latest -f Containerfile .

# Tag image
podman tag myapp:latest localhost/myapp:v1.0

# Push to registry
podman login docker.io
podman push myapp:latest docker.io/username/myapp:latest

# Remove image
podman rmi nginx:alpine

# Prune unused images
podman image prune -a

# Export/Import
podman save -o myapp.tar myapp:latest
podman load -i myapp.tar

# Inspect image layers
podman history myapp:latest
podman inspect myapp:latest | jq '.[0].RootFS.Layers'

Container Operations

# Run detached with restart policy
podman run -d --restart=always --name nginx nginx:alpine

# Run with resource limits
podman run -d --name limited \
    --memory 512m \
    --cpus 1.5 \
    nginx:alpine

# Run with environment
podman run -d --name app \
    -e DATABASE_URL="postgres://localhost/db" \
    -e DEBUG=false \
    myapp:latest

# Run with volume mount
podman run -d --name app \
    -v ./config:/etc/app:ro \
    -v app-data:/var/lib/app \
    myapp:latest

# Execute in running container
podman exec -it app bash
podman exec app cat /etc/os-release

# Copy files
podman cp app:/var/log/app.log ./
podman cp ./config.yml app:/etc/app/

# View logs
podman logs -f app
podman logs --tail 100 --timestamps app

# Resource stats
podman stats app
podman stats --no-stream --format json

# Container inspection
podman inspect app | jq '.[0].State'
podman inspect app --format '{{.NetworkSettings.IPAddress}}'

# Top processes
podman top app

Pod Management (Kubernetes-like)

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

# Run containers in pod
podman run -d --pod mypod --name nginx nginx:alpine
podman run -d --pod mypod --name php php:fpm

# List pods
podman pod ls

# Pod status
podman pod inspect mypod

# Stop/Start pod (all containers)
podman pod stop mypod
podman pod start mypod

# Remove pod and all containers
podman pod rm -f mypod

# Create pod with infra container settings
podman pod create --name webstack \
    --infra-name webstack-infra \
    -p 80:80 -p 443:443 \
    --network bridge

KEY CONCEPT: Pods share network namespace. All containers in a pod can reach each other via localhost.

Networking

# List networks
podman network ls

# Create network
podman network create mynet

# Create with subnet
podman network create --subnet 10.89.0.0/24 --gateway 10.89.0.1 mynet

# Run container on network
podman run -d --network mynet --name app1 nginx
podman run -d --network mynet --name app2 nginx

# Containers can reach each other by name
podman exec app1 ping app2

# Connect existing container to network
podman network connect mynet existing-container

# Disconnect from network
podman network disconnect mynet container

# Inspect network
podman network inspect mynet

# DNS in rootless mode
# Containers use slirp4netns or pasta for networking
podman info | grep -A10 network

# Use pasta (faster than slirp4netns)
podman run --network pasta nginx:alpine

Volume Management

# List volumes
podman volume ls

# Create named volume
podman volume create mydata

# Inspect volume
podman volume inspect mydata

# Use volume
podman run -d -v mydata:/data --name app myapp:latest

# Backup volume
podman run --rm -v mydata:/source:ro -v $(pwd):/backup \
    alpine tar czf /backup/mydata.tar.gz -C /source .

# Restore volume
podman volume create mydata-restored
podman run --rm -v mydata-restored:/dest -v $(pwd):/backup \
    alpine tar xzf /backup/mydata.tar.gz -C /dest

# Remove volume
podman volume rm mydata

# Prune unused volumes
podman volume prune

# Bind mount with SELinux (Fedora/RHEL)
podman run -v ./data:/data:Z myapp  # Private, relabeled
podman run -v ./data:/data:z myapp  # Shared, relabeled

GOTCHA: On SELinux systems, use :Z or :z suffix or get permission denied.

Systemd Integration

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

# With automatic updates
podman generate systemd --name myapp --new --restart-policy=always \
    > ~/.config/systemd/user/myapp.service

# Enable and start (user service)
systemctl --user daemon-reload
systemctl --user enable --now myapp

# Check status
systemctl --user status myapp

# View logs
journalctl --user -u myapp -f

# Generate for pod
podman generate systemd --name mypod --files
# Creates: pod-mypod.service + container-*.service files

IMPORTANT: User services require lingering enabled:

loginctl enable-linger $(whoami)

Without this, user services stop when you log out.

Quadlet (Modern Systemd Integration)

# Quadlet files go in: ~/.config/containers/systemd/
# Extension: .container, .pod, .volume, .network, .kube

# Example: ~/.config/containers/systemd/nginx.container
# ~/.config/containers/systemd/nginx.container
[Unit]
Description=Nginx web server
After=local-fs.target

[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80
Volume=nginx-data.volume:/usr/share/nginx/html:ro
Environment=NGINX_HOST=localhost

[Service]
Restart=always
TimeoutStartSec=300

[Install]
WantedBy=default.target
# ~/.config/containers/systemd/nginx-data.volume
[Volume]
# Reload systemd to pick up quadlet files
systemctl --user daemon-reload

# Start service (quadlet generates the unit)
systemctl --user start nginx

# Check generated unit
systemctl --user cat nginx

# Enable for auto-start
systemctl --user enable nginx

BENEFIT: Quadlet is declarative - no need to manually generate units. Just drop .container files and systemd handles everything.

Kubernetes YAML Generation

# Generate k8s YAML from pod
podman generate kube mypod > mypod.yaml

# Generate from container
podman generate kube mycontainer > deployment.yaml

# Deploy from k8s YAML (single-node)
podman play kube deployment.yaml

# Deploy with custom options
podman play kube --network host deployment.yaml

# Stop deployment
podman play kube --down deployment.yaml

# Replace running deployment
podman play kube --replace deployment.yaml

# Generate with service
podman generate kube --service mypod > mypod-with-service.yaml

USE CASE: Develop locally with podman, then deploy same YAML to k8s cluster.

Podman Compose

# Install podman-compose
pip install --user podman-compose

# Or use system package
sudo pacman -S podman-compose  # Arch

# Run compose file
podman-compose up -d

# Stop
podman-compose down

# Logs
podman-compose logs -f

# Alternative: Use docker-compose with podman socket
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
docker compose up -d

GOTCHA: podman-compose doesn’t support all docker-compose features. Complex compose files may need the docker-compose + socket approach.

Building Images

# Build with Containerfile (or Dockerfile)
podman build -t myapp:latest .

# Build with specific file
podman build -f Containerfile.prod -t myapp:prod .

# Build with build args
podman build --build-arg VERSION=1.0 -t myapp:latest .

# Build without cache
podman build --no-cache -t myapp:latest .

# Build for different architecture
podman build --platform linux/arm64 -t myapp:arm64 .

# Multi-stage build
podman build --target builder -t myapp:builder .

# Squash layers (smaller image)
podman build --squash -t myapp:squashed .
# Containerfile (Podman-native name)
FROM fedora:latest AS builder
RUN dnf install -y golang
COPY . /app
WORKDIR /app
RUN go build -o myapp

FROM fedora:minimal
COPY --from=builder /app/myapp /usr/local/bin/
USER 1000
CMD ["/usr/local/bin/myapp"]

Security Features

# Run without any capabilities
podman run --cap-drop=ALL nginx:alpine

# Add specific capability
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx:alpine

# Read-only filesystem
podman run --read-only nginx:alpine

# With tmpfs for writable paths
podman run --read-only --tmpfs /tmp --tmpfs /var/cache/nginx nginx:alpine

# No new privileges
podman run --security-opt=no-new-privileges nginx:alpine

# Run as non-root user
podman run --user 1000:1000 nginx:alpine

# SELinux contexts (Fedora/RHEL)
podman run --security-opt label=type:container_t myapp

# Seccomp profile
podman run --security-opt seccomp=./seccomp.json myapp

# Inspect container security
podman inspect mycontainer | jq '.[0].HostConfig.SecurityOpt'

# Check for secrets in image
podman history --no-trunc myapp:latest | grep -i "password\|secret\|key"

BEST PRACTICE: Always use --cap-drop=ALL then add only needed capabilities.

Registry Operations

# Login to registry
podman login docker.io
podman login quay.io
podman login ghcr.io
podman login registry.example.com

# Login with credentials
echo $REGISTRY_TOKEN | podman login -u username --password-stdin ghcr.io

# Check login status
podman login --get-login docker.io

# Logout
podman logout docker.io

# Configure registries (search order)
cat ~/.config/containers/registries.conf
# ~/.config/containers/registries.conf
unqualified-search-registries = ["docker.io", "quay.io"]

[[registry]]
location = "docker.io"
[[registry.mirror]]
location = "registry-mirror.example.com"

[[registry]]
location = "registry.example.com"
insecure = true  # Allow HTTP
# Pull from specific registry
podman pull quay.io/prometheus/prometheus:latest

# Push to private registry
podman tag myapp:latest registry.example.com/myapp:latest
podman push registry.example.com/myapp:latest

# List remote tags
skopeo list-tags docker://docker.io/library/nginx

Secrets Management

# Create secret from file
podman secret create db_password ./password.txt

# Create from stdin
echo "mysecretpassword" | podman secret create db_password -

# List secrets
podman secret ls

# Use secret in container
podman run -d --secret db_password myapp:latest

# Secret available at /run/secrets/db_password in container

# Use secret as environment variable
podman run -d --secret db_password,type=env,target=DATABASE_PASSWORD myapp

# Inspect secret metadata (not the value!)
podman secret inspect db_password

# Remove secret
podman secret rm db_password

SECURITY: Secrets are stored encrypted by default. They’re not visible in podman inspect output.

Troubleshooting

# Check system info
podman info

# Verify rootless setup
podman info | grep -A5 rootless

# Check storage
podman system df

# Reset everything (DESTRUCTIVE)
podman system reset

# Debug container startup
podman run --rm nginx:alpine echo "works"
podman logs failing-container

# Check events
podman events --filter event=die --since 1h

# Debug networking
podman run --rm nicolaka/netshoot ping google.com
podman run --rm nicolaka/netshoot dig google.com

# Health check logs
podman healthcheck run mycontainer
podman inspect mycontainer | jq '.[0].State.Health'

# Permission issues (SELinux)
sudo ausearch -m avc -ts recent | audit2why
# Fix: Use :Z or :z volume suffix

# Storage cleanup
podman system prune -a --volumes

# Fix broken storage
podman system reset --force

COMMON ISSUES: 1. "Permission denied" → SELinux, use :Z suffix 2. "No space left" → podman system prune -a 3. "Network unreachable" → Check slirp4netns or pasta

Infrastructure Patterns

# Development Vault (like docker example)
podman run -d --name vault-dev \
    -p 8200:8200 \
    -e VAULT_DEV_ROOT_TOKEN_ID=root \
    --cap-add=IPC_LOCK \
    hashicorp/vault:latest

# PostgreSQL for development
podman run -d --name postgres \
    -e POSTGRES_PASSWORD=devpassword \
    -e POSTGRES_DB=myapp \
    -v postgres_data:/var/lib/postgresql/data \
    -p 5432:5432 \
    postgres:16

# Redis
podman run -d --name redis \
    -v redis_data:/data \
    -p 6379:6379 \
    redis:7-alpine

# Multi-container pod (like k8s)
podman pod create --name dev-stack -p 3000:3000 -p 5432:5432

podman run -d --pod dev-stack --name app \
    -e DATABASE_URL="postgres://localhost/myapp" \
    myapp:latest

podman run -d --pod dev-stack --name db \
    -e POSTGRES_PASSWORD=secret \
    postgres:16
# Both share localhost networking in the pod

# Generate systemd for production
podman generate systemd --name dev-stack --files --new
systemctl --user enable --now pod-dev-stack

Docker Migration Gotchas

# GOTCHA 1: No daemon
# Docker: docker run ... (talks to daemon)
# Podman: podman run ... (direct execution)
# Impact: No `docker events` streaming from daemon

# GOTCHA 2: Network namespace
# Docker: Custom bridge with DNS by default
# Podman rootless: slirp4netns (slower) or pasta
# Fix: Use pods for inter-container communication

# GOTCHA 3: Volume permissions
# Docker: Root in container = root on host volume
# Podman rootless: Root in container = your user on host
# Impact: May need to fix permissions or use --userns=keep-id

# GOTCHA 4: Ports < 1024
# Docker: Works (daemon runs as root)
# Podman rootless: Fails by default
# Fix: sysctl -w net.ipv4.ip_unprivileged_port_start=80
# Or use higher port and reverse proxy

# GOTCHA 5: Compose networking
# Docker Compose: Creates networks, DNS "just works"
# Podman Compose: May need explicit network creation
# Fix: Use pods, or docker-compose with podman socket

# GOTCHA 6: .dockerignore vs .containerignore
# Podman prefers .containerignore but reads .dockerignore
# Best practice: Use both or symlink

Podman Machine (macOS/Windows)

# On macOS/Windows, podman needs a Linux VM

# Initialize default machine
podman machine init

# Initialize with custom resources
podman machine init --cpus 4 --memory 8192 --disk-size 100

# Start machine
podman machine start

# SSH into machine
podman machine ssh

# Stop machine
podman machine stop

# List machines
podman machine ls

# Remove machine
podman machine rm default

# Set machine as default
podman system connection default podman-machine-default

NOTE: On Linux, podman runs natively - no machine needed.