podman
Read this after chapter 1 and chapter 5.
Set runtime: podman and every emitted step switches binary — podman pull,
podman network create, podman run, podman exec, podman ps, and podman save --format docker-archive for --local-image. No docker shim, no compatibility layer.
version: 2service: helloimage: traefik/whoami:latestruntime: podmanhosts: [box.example.com]proxy: { enabled: true, host: hello-podman.example.com, port: 80 }Two podman-only steps (both idempotent)
Section titled “Two podman-only steps (both idempotent)”[runtime-check]— rootless boot persistence. Enablessystemctl --user enable --now podman-restart.serviceandloginctl enable-linger $USER, each behind a guard, so--restart unless-stoppedcontainers actually come back after a reboot. Docker emits no such step.[port-guard]— informational host check ofnet.ipv4.ip_unprivileged_port_start, plus the note naming the fix. It never aborts: the bind is fixed inside the container, because the kamal-proxy image runs as a non-root user and is created with--sysctl net.ipv4.ip_unprivileged_port_start=0on podman. Docker never gets that flag.
bundant bootstrap — install podman on the host
Section titled “bundant bootstrap — install podman on the host”A host that has nothing but sshd (and root) needs one command before its first deploy:
bundant bootstrap -f deploy.yml # per host in deploy.yml (BUNDANT_HOSTS overrides)bundant bootstrap --dry-run # print the exact commands, touch nothingbundant bootstrap --skip-smoke # skip the pull+run checkOne probe round trip reads /etc/os-release plus the tools actually installed, then:
- installs podman when missing, choosing the manager the distro ships:
apt-get(Debian/Ubuntu/Mint/Raspberry Pi OS/Kali),dnf(Fedora/RHEL 8+/CentOS 8+/ Rocky/Alma/Oracle/Amazon Linux 2023),yum(CentOS 7),apk(Alpine),pacman(Arch/Manjaro/EndeavourOS),zypper(openSUSE/SLES). The distro id wins,ID_LIKEis the fallback, and it degrades to whatever manager is installed; a distro we cannot map fails withinstall podman manuallyinstead of guessing. - writes the registry search path
/etc/containers/registries.conf.d/999-bundant.conf(unqualified-search-registries = ["docker.io"]) — once, never clobbered; without itpodman run alpinedies withshort-name … did not resolve. - enables
podman-restart.serviceso--restart unless-stoppedcontainers come back after a reboot (skipped on non-systemd hosts). - smoke-tests
podman run --rm docker.io/library/hello-world, and inside LXC the failure text namesnesting=1.
Everything is idempotent: a ready host costs one probe and reports
registry search path already configured / podman version … / smoke test passed.
Step output is shown only when a step fails, so apt’s noise never floods the terminal.
Hosts that are themselves containers (Proxmox / LXC)
Section titled “Hosts that are themselves containers (Proxmox / LXC)”- Create the LXC with
nesting=1(andkeyctl=1for rootless) — podman needs them for nested cgroups and keyrings. - If overlay mounts are refused, check the storage driver (
fuse-overlayfsorvfs). - Nothing else: the image transfer stays
save | gzip | ssh | gunzip | load, and since podman saves asdocker-archivea host can switch between docker and podman freely.
Air-gapped hosts and the proxy image
Section titled “Air-gapped hosts and the proxy image”The proxy runs docker.io/basecamp/kamal-proxy:latest (Docker Hub is the only source).
A host that cannot reach it needs no build — pull the image somewhere that can and move
it with the same pipeline:
docker pull docker.io/basecamp/kamal-proxy:latestdocker save docker.io/basecamp/kamal-proxy:latest | gzip -1 \ | ssh you@box.example.com "gunzip | podman load"Podman’s pull policy is missing, so once the tag exists locally it is used as-is;
point proxy.image at your own tag if you retag it.