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.