Podman — running CodeNavi without Docker¶
CodeNavi's installer and lifecycle scripts work with Podman as well as Docker. On a podman-only host — Fedora and RHEL ship podman by default, many RHEL-family organizations do not permit Docker, and Debian and Ubuntu package podman too — the stock installer detects podman, drives it through its docker-compatible API socket, and configures the install accordingly, including rootless operation under SELinux Enforcing.
AI agent installing this for someone? Follow https://codenavi.com/docs/setup-agent.md instead. It is written for agents to execute, step by step.
This path is verified end to end on Fedora with rootless podman 5.x: signed
package verification, image load, GPU access (AMD ROCm), the hardened compose
profile (read_only, cap_drop: ALL, no-new-privileges), indexing on GPU,
and search.2
Prerequisites¶
One-time host preparation — install podman and a compose provider.
On Fedora:
sudo dnf install podman podman-compose
On RHEL 9, Rocky Linux 9, AlmaLinux 9 and other RHEL-family systems, podman-compose is
not in the base repositories, and the one EPEL carries (1.5.0) cannot parse this project's
compose file — the install reports success through every phase and then compose up dies with
could not parse mount. Take podman from the base repo and the compose provider from PyPI:
sudo dnf install podman epel-release
sudo dnf install pipx
pipx install podman-compose # 1.6 — into ~/.local/bin, ahead of /usr/bin
podman-compose --version # want 1.6 or newer
Verified on a Rocky Linux 9.8 cloud image with rootless podman 5.8 and an NVIDIA L4 under SELinux Enforcing (2026-09-13). The installer probes the provider and stops before downloading anything if it cannot render the compose file.
On Debian, Ubuntu and other apt systems, take podman from the archive but the compose
provider from PyPI:
sudo apt install podman pipx
pipx install podman-compose
Do not use apt install podman-compose on Debian 13 or Ubuntu. That package is
podman-compose 1.3.0, which never expands ${VAR:-default} inside a compose file's
environment: block (the RHEL-family EPEL package above fails differently, on the mount). The container then starts with the literal text
${ENCODE_BATCH_SIZE:-} as a value and crash-loops — after an install that reported
success through every phase. The installer now probes the provider and stops before
downloading anything (CN-676). pipx puts 1.6 in ~/.local/bin, which podman picks
ahead of /usr/bin; confirm with podman-compose --version.
That is the only package step. The installer itself enables your user's podman API socket
(systemctl --user enable --now podman.socket) and finds it automatically. Any
Compose v2 provider works in place of podman-compose — a standalone
docker-compose binary on PATH is also fine.
Podman needs a compose provider; it does not ship one. podman compose is a
thin wrapper that hands the work to whichever provider it finds. With none
installed, the install stops early and tells you so — before downloading the
image, not after.
How far each distribution is verified. The end-to-end checks above — signed
package verification, image load, GPU access, indexing, search — are run every
release on Fedora with podman 5.x and podman-compose 1.6. Debian 13 with the same
podman-compose version is verified as of 1.3.36 — the code path is identical; what
differs is only which provider the archive gives you, which is why the package advice
above matters. A standalone Compose v2 binary on PATH works equally well; podman
prefers it over podman-compose.
Keep it running after you log out. A rootless container lives inside your
user's systemd manager, and that manager is stopped when your last login session
ends — taking CodeNavi with it, whatever restart policy the compose file carries.
The symptom is a container that exits with status 143 seconds after you log
out, with a clean log and no error. Enable lingering once, for the account that
owns the install:
sudo loginctl enable-linger "$USER" # root (or a polkit rule) — a plain user gets "Access denied" on headless and WSL hosts
loginctl show-user "$USER" --property=Linger # expect Linger=yes
A systemd user session is a prerequisite, not a nicety. Rootless podman registers the
image's health check with your systemd manager; with no user session (a WSL distro with
systemd off, a su/sudo -u shell, a stripped host) every install phase succeeds and only the
start fails, in podman's words: unable to get systemd connection to add healthchecks or
systemd slice received as cgroup parent when using cgroupfs. cgroup_manager="cgroupfs" in
containers.conf does not clear it. Check ls /run/user/$(id -u)/systemd — it must exist. On
WSL put [boot] / systemd=true in /etc/wsl.conf and wsl --shutdown; elsewhere log in as
the owning account directly. From 1.3.43 the installer says this in its Environment phase,
before any download.
From 1.3.25 the installer does this for you on a rootless install: it reads your
linger state, enables it when the system lets it, and otherwise prints the exact
loginctl enable-linger command on its completion screen. The manual steps stay
valid for older packages and for the boot-time restart below.5
To have CodeNavi come back on its own after a reboot, podman's restart service
must be enabled for that user. Lingering brings the user manager back at boot;
it does not by itself restart the container. From 1.3.47 the installer enables
the service for you and runs podman installs with restart: always (the
docker-compose.podman-restart.yml overlay), because podman before 5.8.2 only
restarts always containers at boot — RHEL 9, Debian 13 and Ubuntu 24.04 ship
older podman. One consequence: a CodeNavi you stopped by hand with
podman stop starts again at the next boot. On an older package, enable the
service yourself:
systemctl --user enable podman-restart.service
and re-run the installer over the install folder once, so it adds the overlay.
Run the installer from a real login session of the owning account. Rootless
podman talks to that account's own systemd manager and D-Bus session. Switching
users with su or sudo -u keeps the calling user's XDG_RUNTIME_DIR and
DBUS_SESSION_BUS_ADDRESS, so podman looks for a session under the wrong uid and
warns no systemd user session available and
dial unix /run/user/<other uid>/bus: connect: permission denied, then falls
back to the cgroupfs manager. Log in as the account directly instead — an ssh
login, a console login or machinectl shell user@ all start a proper session.
If you must stay in a switched shell, point the two variables at your own uid
first (id -u prints it):
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
What the installer does differently under podman¶
- Engine detection — when
dockeris absent andpodmanis present,setup.shuses podman through its docker-compatible socket. The choice is recorded in.envasCODENAVI_ENGINE=podman, andupdate.sh,apply-update.shand thecodenaviCLI follow it. - Hosts with both Docker and podman default to Docker. The installer never guesses between two working engines. To install on podman anyway, set the engine on the command line:
CODENAVI_ENGINE=podman ./setup.sh
The pre-flight table's Engine row shows which one was picked, and the
.env line CODENAVI_ENGINE=podman makes every later update.sh and
codenavi command follow it — you set the variable once, at install time. If
an install already ran without it and landed on Docker, stop that install
(docker compose down in its directory) and re-run setup.sh with the
variable set. Docker and podman keep separate image stores, so the re-run
needs the image tarball next to setup.sh again — re-extract the package, or
install with KEEP_IMAGE_TAR=1 the first time so the tarball is not deleted.
- Rootless user mapping — rootless podman maps your host user to uid 0
inside the container's user namespace, so the installer writes
HOST_UID=0 / HOST_GID=0 to .env. This grants no privilege: container
"root" under rootless podman is your unprivileged user. (If you ever see
a Cannot write to /app/data crash-loop on a hand-migrated install, this
setting is the fix.)
- SELinux — every podman install runs under SELinux Enforcing without booleans
or policy changes. The container runs with SELinux label separation off
(security_opt: label=disable): the two GPU overlays carry it for their device
nodes,13 and from 1.3.25 the installer stacks
docker-compose.podman.yml on the CPU-only stack for the same reason — a
confined container cannot write the bind-mounted data/, index/, lancedb/
and logs/ directories or read the codebase, because they carry the
home-directory label.4 What still confines the container: the rootless
user namespace (container root is your unprivileged user), the read-only
root filesystem, all capabilities dropped and no-new-privileges.
An existing CPU-only install made with an older package gains the overlay on
its next bundle re-extract (./setup.sh migrates COMPOSE_FILE). To fix such an
install by hand before that: create docker-compose.override.yml containing
services: { codenavi: { security_opt: [label=disable] } } and append
:docker-compose.override.yml to COMPOSE_FILE in .env — compose ignores
the override file when COMPOSE_FILE is set, and every install sets it — then
docker compose up -d --force-recreate. Relabelling with chcon works too but
does not survive a restorecon, so prefer the overlay.
Day-to-day operation is identical: docker compose commands in the docs map
to podman compose, updates apply with the same ./update.sh, and the same
signed artifacts are verified with the same bundled cosign key.
GPU notes¶
- AMD (ROCm) — verified working rootless; the compose overlay's device
mappings (
/dev/kfd,/dev/dri) apply unchanged. - NVIDIA — podman reaches NVIDIA GPUs via CDI, not Docker's runtime,
and the installer stacks
docker-compose.podman-nvidia.ymlon the GPU compose files for that reason: podman's docker-compatible API ignores theruntime: nvidialine the Docker path relies on, so the card is requested as a CDI device instead. (Installs made with a package older than 1.3.24 on podman + NVIDIA were running on the CPU — re-extract the current package and re-run./setup.sh; it migratesCOMPOSE_FILEand restarts on the GPU.)
One-time host preparation, before running the installer. The toolkit is not in the distribution's own repositories, so NVIDIA's has to be added first.
On Fedora, RHEL and other dnf systems:
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo \
| sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo >/dev/null
sudo dnf install -y nvidia-container-toolkit
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
nvidia-ctk cdi list # expect nvidia.com/gpu=0 … and nvidia.com/gpu=all
On Debian, Ubuntu and other apt systems:
sudo install -d -m 0755 /usr/share/keyrings
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
nvidia-ctk cdi list # expect nvidia.com/gpu=0 … and nvidia.com/gpu=all
(The installer prints whichever of these matches your package manager whenever the probe fails, so you do not have to choose.)
Regenerate the spec after any GPU or driver change. /etc/cdi/nvidia.yaml
is a static snapshot of the cards present when it was written, and nothing
refreshes it. Add a second GPU later and podman cannot reach it at all: the
new index is absent from the spec, so CODENAVI_GPU_DEVICE=<index> fails to
resolve and the container will not start — and even all still maps only to
the card that was there before. Re-run the cdi generate line above, confirm
the new index with nvidia-ctk cdi list, then restart. Docker is unaffected:
its runtime hook enumerates the cards live.
On rootless podman, if the installer's probe still reports NVML
Insufficient Permissions with the CDI spec in place, uncomment and set
no-cgroups = true in /etc/nvidia-container-runtime/config.toml (the
default lets the toolkit try to manage cgroups, which an unprivileged user
cannot) and re-run.
The installer names the missing piece when it stops, and — new in 1.3.24 — checks after start-up that the server really loaded on the GPU and warns when a GPU install came up on the CPU. It never silently falls back to CPU indexing.
Limitations¶
- The from-source developer build (
./setup.shwithout--airgapon a source checkout) remains Docker-only; podman support covers the customer install and update path. - On rootless podman, published ports are forwarded over IPv4 — use
127.0.0.1rather thanlocalhostin scripts if your resolver prefers IPv6 (the installer and CLI already do).
A note on macOS¶
On macOS, podman machine and Docker Desktop both run Linux containers inside
a VM, and neither exposes Apple-silicon GPU acceleration to the container — so
the engine compatibility on this page applies there too, but indexing stays
CPU-bound and slow either way. The full macOS story (great search/MCP client,
poor indexing host, and the two production-quality ways to get an index onto a
Mac) is in CodeNavi on macOS.