Skip to content

Provisioning a Podman Host

Tej Pochiraju edited this page Jun 14, 2026 · 8 revisions

Provisioning a Podman Host

podman-api does not run containers itself — it drives a rootless podman.socket on each target host over SSH. This page turns a fresh Linux box into such a target. Pick a distro from Supported host OS below first; command examples are for AlmaLinux 10 (the recommended distro) and were validated end-to-end on AlmaLinux 10.2 (2026-06-05).

Supported host OS

Managed hosts must run podman ≥ 5.6.0: cold-copy migrate/evacuate streams volumes through the libpod volume export/import API, which first shipped in podman 5.6.0 (#85). The daemon enforces this — it refuses to boot against a reachable host below the floor (all offenders reported in one error), and hosts that were unreachable at boot are version-checked on first use (operations fail with host_version_unsupported instead of a confusing 404).

Distro podman (stock repos) Supported?
AlmaLinux 10 (recommended) 5.6.0 → 5.8.2 (AppStream)
Rocky Linux 10 5.6.0 → 5.8.2 (AppStream)
Fedora 41/42 5.6.2 / 5.8.2
Ubuntu 24.04 LTS 4.9.3
Debian 13 (trixie) 5.4.2
Rocky Linux 9 / RHEL 9.x ~5.4

Recommendation: AlmaLinux 10. Both RHEL-10 rebuilds ship podman 5.8.2 (exact parity with the daemon's pinned bindings); AlmaLinux wins on independent security cadence, foundation governance, and broader hardware support. Rocky 10 is a fine second choice if exact 1:1 RHEL parity is ever required.

Escape hatch (discouraged): Debian/Ubuntu can reach the floor via the upstream openSUSE OBS podman repo, but that adds per-host third-party repo maintenance we don't want on a managed fleet. Prefer reinstalling on AlmaLinux 10.

0. Facts to capture first

SSH in and note these — they go straight into the hosts/<id>.yaml later:

ssh almalinux@<host>
whoami; id -u          # the user and its uid (socket path uses the uid)
cat /etc/os-release    # distro/version
uname -m               # arch

1. Install podman (rootless deps included)

# AlmaLinux / Rocky 10 (recommended):
sudo dnf install -y podman

podman --version       # must be >= 5.6.0 (the daemon refuses older hosts)
podman info --format '{{.Host.Security.Rootless}}'   # expect: true

2. Enable the rootless socket, persistently

The socket must survive logout, so enable lingering for the user, then start the user podman.socket:

sudo loginctl enable-linger "$USER"
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
systemctl --user enable --now podman.socket

systemctl --user is-active podman.socket      # active
ls -l "$XDG_RUNTIME_DIR/podman/podman.sock"   # e.g. /run/user/1000/podman/podman.sock
podman ps                                     # smoke test

The socket path is /run/user/<uid>/podman/podman.sock — record it.

If podman ps warns about "no systemd user session", that's expected over a non-login SSH session; lingering keeps the socket alive regardless.

3. SSH identity for podman-api

podman-api connects as ssh://<user>@<host><socket> using a named identity file. Use a key the daemon host holds and the target authorises:

# on the podman-api host, confirm the key the server accepts:
ssh -v <user>@<host> true 2>&1 | grep -i "Server accepts key"

Best practice is a dedicated key per host (e.g. prod-1.id_ed25519) so you can rotate one target without touching others.

4. Write the host file

Drop a file into the directory podman-api scans with -hosts-dir:

# hosts/prod-1.yaml
id: prod-1
addr: almalinux@<host>                    # ssh://user@host (default port 22)
socket: /run/user/1000/podman/podman.sock
ssh_key: /etc/podman-api/keys/prod-1.id_ed25519
labels:
  env: prod
  region: ap-south-1

addr: unix connects to a local socket directly (dev only). Anything else opens an SSH tunnel using ssh_key. Real host files are git-ignored; commit only a sanitised example.

5. Verify through podman-api

With the daemon running (see Deploying):

curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:8080/hosts/prod-1/healthz
# {"status":"ok"}
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:8080/hosts/prod-1
# ... "podman_version":"5.x.y" ...

A successful podman_version means the SSH tunnel, identity, and socket path are all correct. A full apply→list→delete then confirms play kube, secrets, and port mapping — see Operating.

6. Optional: run the integration suite against this host

The -tags=integration tests use a local socket. To exercise them on this box directly, install Go and run make test-integration there, or rely on the podman-in-podman CI job (Building).

7. Ingress: allow rootless privileged-port binding (only if using ingress)

The optional ingress feature (-ingress-enabled, see Operating) runs a managed Caddy pod that must publish :80 and :443 on the host. Under rootless podman those ports are blocked until you lower the unprivileged-port floor, persistently:

echo 'net.ipv4.ip_unprivileged_port_start=80' | \
  sudo tee /etc/sysctl.d/99-podman-ingress.conf
sudo sysctl --system

sysctl net.ipv4.ip_unprivileged_port_start   # => ... = 80

Without it the Caddy pod fails to start (cannot expose a privileged port). Hosts that never run ingress can skip this. Running Caddy rootful is not supported — podman-api drives every host over the single rootless user socket from § 2.

For HTTP-01 to issue certs, the host's :80/:443 must be reachable from the internet and each instance domain must already resolve to this host (operator-managed DNS).

8. Optional: configure insecure private registries

If any workload pulls images from a private insecure registry (plain HTTP or self-signed TLS), the target host must declare that registry as trusted before apply or migrate's apply-on-dest can pull images. Without it, both fail with a TLS ping error (the daemon surfaces this as pinging container registry … no such host or x509: certificate signed by unknown authority).

Create a drop-in config file on the target host:

mkdir -p ~/.config/containers/registries.conf.d

cat > ~/.config/containers/registries.conf.d/<registry-name>.conf <<'EOF'
[[registry]]
location = "<host>:<port>"
insecure = true
EOF

Replace <host>:<port> with your registry address (e.g. 100.64.0.23:5000). The filename (e.g. otp-registry.conf) is arbitrary — podman merges every .conf file found in that directory.

Verify the registry is now reachable:

podman pull <host>:<port>/<image>:<tag>

This is a per-host setting. Every managed host that runs workloads from the private registry needs its own copy of this file — it is not inherited from the daemon host.

9. On the daemon host: a writable state-db directory

This is about the podman-api daemon host (where you run the binary), not the target above. The state store is always on, and -state-db is its location (default /var/lib/podman-api/state.db). Its parent directory must exist and be writable by the user the daemon runs as.

The daemon creates the parent directory on startup, but a non-root run pointed at the privileged default path will fail with a permission error. Either run as a user that can write /var/lib/podman-api/ (the bundled installer's podman-api system user owns it), or point -state-db at a writable path:

# example for a non-root, non-installer run:
install -d -m 0750 -o "$USER" "$HOME/.local/share/podman-api"
podman-api ... -state-db="$HOME/.local/share/podman-api/state.db"

The -spec-key-file is optional — the store opens fine without it, and templates and no-secret deploys work key-less. You only need a key to persist secrets (secret-bearing deploys are rejected without one); see Deploying.

Related