Introduction
Podman, Red Hat's daemonless container tool, is revolutionizing deployments in 2026 with its native rootless mode, OCI compatibility, and Kubernetes integration. Unlike Docker, Podman skips the privileged daemon, eliminating root vulnerabilities and simplifying multi-user production environments.
This expert tutorial targets senior DevOps engineers: from multi-container pods to systemd quadlets for persistent services, including TLS secrets and podman play kube. Why it matters? In production, 70% of breaches stem from daemons; Podman drops that risk to zero while supporting Compose and Buildah.
Master cgroups v2 storage config, macvlan networks, and monitoring with podman stats. By the end, deploy a scalable rootless NGINX-Postgres stack. Estimated time: 2 hours for a pro setup worth bookmarking. (142 words)
Prerequisites
- OS: Fedora 40+, RHEL 9+, Ubuntu 24.04 (cgroups v2 enabled:
systemd.unified_cgroup_hierarchy=1in GRUB) - User: Non-root with subUID/subGID (check
id -uand/etc/subuid) - Knowledge: Docker CLI, Kubernetes YAML, systemd units
- Tools:
podman5.0+ (dnf install podmanorapt install podman), optionalbuildah,skopeo - Disk space: 10GB for images/layers
Install and Configure Rootless Podman
#!/bin/bash
# Fedora/RHEL
dnf install -y podman podman-compose buildah skopeo fuse-overlayfs
# Ubuntu
# apt update && apt install -y podman podman-compose buildah skopeo fuse-overlayfs
# Enable linger for persistent services
loginctl enable-linger $USER
# Config rootless storage (fuse-overlayfs for high perf)
mkdir -p ~/.config/containers
cat > ~/.config/containers/storage.conf << EOF
[storage]
driver = "overlay"
runroot = "/run/user/$(id -u)/containers"
graphroot = "/home/$(id -u)/.local/share/containers/storage"
[storage.options.overlay]
ignore_sizes = true
mount_program =
/usr/bin/fuse-overlayfs
mountopt = "nodev=ro,nosuid,size=100000k"
EOF
# Verify
podman info | grep -E 'cgroup|rootless'This script installs Podman and dependencies, sets up fuse-overlayfs storage for high-performance rootless mode (avoids perf loss vs native overlayfs). enable-linger enables persistent pods after logout. Check podman info: must show 'rootless' and cgroup v2. Pitfall: without subUID, podman run fails with 'permission denied'.
Verification and First Container
Run the script above. Think of Podman as your personal 'Docker' without root—each user gets an isolated namespace. Test with podman run --rm alpine echo 'Podman OK'. Next: pods for multi-service setups.
Create a Multi-Container Pod
#!/bin/bash
# Create pod with NGINX + logging sidecar
podman pod create --name monpod -p 8080:80 --hostname monpod
# Main NGINX
podman run -d --pod monpod --name nginx \
docker.io/nginx:alpine \
sh -c 'echo "<h1>Podman Expert 2026</h1>" > /usr/share/nginx/html/index.html && nginx -g "daemon off;"'
# Sidecar logs to shared volume
podman run -d --pod monpod --name logsidecar \
docker.io/alpine \
sh -c 'while true; do echo "$(date)" >> /shared/logs.txt; sleep 5; done'
# Volume for intra-pod sharing
podman volume create sharedlogs
podman run -d --pod monpod --name logsidecar \
-v sharedlogs:/shared \
docker.io/alpine \
sh -c 'while true; do echo "$(date)" >> /shared/logs.txt; sleep 5; done'
# Inspect
podman pod ps
podman logs monpod -fCreates a Kubernetes-like pod with NGINX exposed on 8080 and a persistent logging sidecar via volume. --pod ties containers to the same network/PID/cgroup. Access http://localhost:8080. Pitfall: without volume, logs are lost on restart; use podman volume ls to list.
Advanced Pod and Network Management
Pods share IPC/net/UTS like a mini-K8s. For production, add macvlan: podman network create -d macvlan -o parent=eth0 macnet. Analogy: pod = shared apartment, containers = communicating rooms via localhost.
Build Custom Multi-Stage Image
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/server .
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/bin/server .
EXPOSE 3000
USER 1001
CMD ["./server"]Containerfile (not Dockerfile) for rootless multi-stage Go app build. USER 1001 ensures non-root security. Build: podman build -t monapp:v1 .. Run: podman run -d -p 3000:3000 --userns=keep-id monapp:v1. Pitfall: without CGO_ENABLED=0, binary too large; inspect layers with podman image inspect.
Build and Push with Podman
#!/bin/bash
# With Containerfile above
podman build -t localhost/monapp:v1 .
podman run -d --name testapp -p 3000:3000 monapp:v1
curl localhost:3000
# Macvlan network for prod-like setup
podman network create -d macvlan -o parent=lo macnet
podman network exists macnet && podman run -d --network macnet --ip 192.168.100.10 monapp:v1
# Export OCI to registry (e.g., Quay.io)
# podman tag monapp:v1 quay.io/user/monapp:v1
# podman push quay.io/user/monapp:v1
podman ps -aBuilds, runs with userns keep-id (maps host UID), macvlan network for dedicated IP. Curl tests endpoint. Pitfall: without --userns=keep-id, host volumes fail in rootless; clean up with podman system prune.
Kubernetes Integration with Play Kube
Podman natively imports Kubernetes YAML. Create pod-kube.yaml for complex stacks.
Podman Play Kube from YAML
apiVersion: v1
kind: Pod
metadata:
name: kubepod
labels:
app: demo
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- mountPath: /shared
name: sharedvol
- name: busybox
image: busybox:1.36
command: ['sh', '-c', 'while true; do echo "Log $(date)" >> /shared/app.log; sleep 10; done']
volumeMounts:
- mountPath: /shared
name: sharedvol
volumes:
- name: sharedvol
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: kubepod-svc
spec:
selector:
app: demo
ports:
- port: 80
targetPort: 80Valid Kubernetes Pod+Service YAML. podman play kube pod-kube.yaml deploys rootless. Expose with podman play kube --service kubepod-svc. Pitfall: emptyDir volumes non-persistent; map host volumes with hostPath.
Run Play Kube and Monitor
#!/bin/bash
podman play kube pod-kube.yaml
podman kube play --service kubepod-svc
# Monitor
podman pod ps
podman stats kubepod
podman logs kubepod/nginx
# Scale (Podman 5+)
podman play kube --replicas 3 pod-kube.yaml
# Cleanup
podman kube down pod-kube.yamlDeploys YAML, exposes service, shows cgroup/CPU stats. Scales replicas for HA. Pitfall: single instance without --replicas; use podman events --filter pod=kubepod for live logs.
Quadlets for Systemd Integration
Quadlets are auto-generated systemd units from .container/.pod files. Place in ~/.config/containers/systemd/. Analogy: cron meets Docker Compose.
Quadlet Pod with Secrets
[Pod]
PodName=monpod
PublishPort=8080:80
PublishPort=5432:5432
Network=macnet
[Container]nginx
Image=docker.io/nginx:alpine
Environment=NGINX_PORT=80
[Container]postgres
Image=postgres:16-alpine
Environment=POSTGRES_PASSWORD=secret123
Environment=POSTGRES_DB=appdb
Volume=pgdata:/var/lib/postgresql/data
[Service]
Restart=alwaysQuadlet .pod deploys NGINX+Postgres with persistent volume and custom network. Secrets via env (improve with podman secret). Run: systemctl --user daemon-reload && systemctl --user start monpod. Pitfall: forget daemon-reload, unit invisible.
TLS Secrets and Systemd Start
#!/bin/bash
# Create TLS secrets
podman secret create tls-cert cert.pem
podman secret create tls-key key.pem
# Add to quadlet (edit monpod.pod)
# [Container]nginx
# Secrets=tls-cert:/etc/ssl/cert.pem=tls.crt
# Secrets=tls-key:/etc/ssl/key.pem=tls.key
# Reload and start
systemctl --user daemon-reload
systemctl --user enable --now monpod
systemctl --user status monpod
journalctl --user -u monpod -f
# Auto-start on boot (linger already enabled)Podman secrets stored encrypted in RAM, mounted read-only. Edit quadlet for TLS. journalctl provides unified logs. Pitfall: secrets non-persistent without podman secret create --driver file.
Best Practices
- Rootless only: Always use
--userns=keep-idfor host volumes, audit withpodman system migrate. - Quadlets > Compose: Prefer systemd for prod HA; migrate with
podman generate systemd. - Isolated networks: Create per-app
podman network create --driver bridge private. - Auto pruning: Add weekly cron
podman system prune -af. - Monitoring:
podman stats --no-stream+ Podman Prometheus exporter.
Common Errors to Avoid
- Cgroup v1: Pods fail; fix GRUB
systemd.unified_cgroup_hierarchy=1+ reboot. - Missing subUID:
podman runcrashes; edit/etc/subuidwithusermod --add-subuids 100000-165535 $USER. - Slow fuse-overlayfs: On SSD, force
overlayif kernel 6+ supports rootless overlay. - Root port bind: Ports <1024 refused rootless; use
sysctl net.ipv4.ip_unprivileged_port_start=80.
Next Steps
- Official docs: Podman Desktop for expert GUI.
- Advanced: Podman Desktop extensions.
- Training: Master Podman in prod with our Learni DevOps courses.
- Resources:
podman machinefor Mac/Windows,podman podman-desktopfor VSCode.