diff --git a/TODO.md b/TODO.md index 88b89ca..ab8e59f 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,7 @@ - [x] git — config, daily workflow, stash, remotes, branching, rebase, bisect, recovery - [x] ansible — playbook patterns, inventory, vault, variables, roles -- [ ] podman — build, run, compose, volumes +- [x] podman — images, containers, build, volumes, networks, pods, compose, systemd, quadlet - [x] jq — filters, select, map, slurp, recipes, @format - [x] curl — methods, headers, auth, upload, download, cookies, -w format - [x] systemd — units, journalctl, timers, targets, dependencies, hardening diff --git a/topics/podman.md b/topics/podman.md new file mode 100644 index 0000000..755befa --- /dev/null +++ b/topics/podman.md @@ -0,0 +1,403 @@ +# 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 + +```bash +# 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 # force +podman image prune # remove dangling +podman image prune -a # remove all unused +``` + +## Containers + +```bash +# 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 + +```bash +-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 + +```dockerfile +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 + +```bash +# 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 + +```bash +# 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 + +```bash +# 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 + +```bash +# 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 + +```yaml +# 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: +``` + +```bash +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 + +```bash +# 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 + +```ini +# ~/.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 +``` + +```bash +systemctl --user daemon-reload +systemctl --user start myapp.service +``` + +## Cleanup + +```bash +# 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 + +- `systemd` — service management for container persistence +- [Podman docs](https://docs.podman.io/) +- [Quadlet docs](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)