Skip to content

Podman for Mjolnir

This is the operational contract for a host that runs Mjolnir local-podman targets. For a coding-agent handoff, run mj setup instructions --platform linux and mj doctor --json, then give the instructions page plus the output of mj doctor --json to the coding agent. The host is ready only when every postcondition in Verification passes for the same unprivileged user that will run mj.

Mjolnir supports Podman 4.0.0 or newer. Version 4 is the minimum because Mjolnir’s local target relies on the mature rootless user-namespace behavior and CLI interfaces that Mjolnir probes (podman info and podman unshare). Podman 3.x is not a supported Mjolnir runtime.

For a target such as:

[targets.podman]
kind = "local-podman"
image = "localhost/mjolnir/agent-dev:latest"

Mjolnir invokes the local podman CLI as the user running Mjolnir; it does not use sudo, a shared Podman socket, or a remote Podman connection. Before each local-podman session and while mj setup evaluates Podman, Mjolnir performs these fast runtime checks:

Terminal window
podman --version
podman info --format '{{.Host.Security.Rootless}}'
podman unshare cat /proc/self/uid_map

For a session, Mjolnir starts a detached, labeled container from the configured image, uses podman exec for the worker, Git, harness, and clone commands, and mounts a dedicated named volume at /workspace. Each session gets a different volume, so concurrent builds never share a Cargo target directory. On stop, Mjolnir first takes and verifies the checkpoint, stops the exact container, and marks the session stopped. It then removes the container, workspace volume, and Git-cache snapshot in a supervised background operation. A slow storage removal therefore does not keep the session logically running, and its current cleanup stage remains visible. No extra host service or privileged helper is required for this default.

workspace_storage can override that policy on a Podman target:

# Default; this line can be omitted.
workspace_storage = { kind = "podman-volume" }
# Legacy behavior: keep /workspace in the container writable layer.
workspace_storage = { kind = "container-layer" }

The container-layer mode exists for compatibility. It makes podman rm delete the whole workspace tree and can therefore make cleanup slow after large builds.

Hosts with native fast dataset deletion can opt into a small helper protocol. For example, the repository’s mj-zfs-workspace-helper.sh creates one ZFS child dataset per session. Copy it to a root-owned host path, edit its fixed dataset and mount roots, and grant the Podman user passwordless sudo for that helper command only. Then configure the target with the helper’s mount root and argv:

workspace_storage = { kind = "host-helper", root = "/mnt/nvme/mj-workspaces", helper = ["sudo", "-n", "/usr/local/libexec/mj-zfs-workspace-helper"] }

Mjolnir invokes HELPER create RESOURCE, HELPER status RESOURCE, and HELPER destroy RESOURCE. The example validates the deterministic resource name and confines all operations beneath fixed roots; it is an example rather than an installed component. The helper-created directory is mounted as the session’s complete /workspace, not as a shared Cargo cache.

The default pull_policy = "auto" starts a session from the image the host already has, and pulls only when the host has no copy at all. Instead of pulling during a launch, the daemon runs podman pull for every remote :latest image once an hour and then podman image prune -f, so a session never waits on a multi-gigabyte download and dangling layers do not pile up. Versioned and local tags stay cached, and a digest-pinned image is never replaced. Set pull_policy to always, newer, missing, or never to override that inference; always and newer pull during the launch as well. Podman’s newer policy retains a cached image when its registry is temporarily unavailable. mj setup additionally creates, executes true in, and removes a disposable container from the configured image. The fast runtime probes themselves never pull an image; a pull happens in the daemon’s background refresh, or during provisioning when the host has no copy of the image or the target sets an explicit always or newer policy.

For GitHub-backed bundles, Mjolnir keeps bare object mirrors under ~/.cache/mjolnir/git/mirrors on the Podman host. A launch refreshes the mirror, creates a session-specific local snapshot under ~/.cache/mjolnir/git/sessions, and mounts only that snapshot read-only into the container. The normal clone then uses it as a Git object reference, avoiding repeated object downloads while preserving the image’s checkout behavior. Git object files are immutable, so local snapshots can share them with hardlinks; filesystem reflink or ZFS permissions are not required.

The cache is opportunistic. Missing host Git, authentication trouble, or a cache preparation failure produces a launch notice and falls back to the ordinary in-container clone. A first cache miss still downloads the complete mirror; later launches fetch only updates. Mjolnir removes session snapshots after their owning container and prunes mirrors unused for 30 days, then applies a 20 GiB least-recently-used soft cap. Cache directories are private to the Podman user because they may retain objects from private repositories. You can remove ~/.cache/mjolnir/git/mirrors while no launch is updating it; do not remove the sessions directory while managed containers are running.

mj doctor --json runs those three checks only when a local-podman target exists, and then checks podman image exists for each configured local-podman image. mj doctor --json --smoke replaces that presence check with the full disposable run/exec/remove test, so it automates Verification sections 3 and 4 for every configured image.

An ssh-podman target gets the same probes and the same smoke test, each wrapped in a noninteractive ssh call to the configured host. Every remediation below then applies on that remote host, as the user that SSH logs in as.

Mjolnir’s bundled agent-development image is published at ghcr.io/brokkai/mjolnir/agent-dev:latest (multi-arch: linux/amd64 and linux/arm64, public, no authentication needed to pull). Pull it directly:

Terminal window
podman pull ghcr.io/brokkai/mjolnir/agent-dev:latest

Building it locally remains a supported alternative, for example to customize the image or to work offline:

Terminal window
podman build --pull=always \
--file containers/Containerfile.agent-dev \
--tag localhost/mjolnir/agent-dev:latest \
containers

Run all podman commands below as the normal Mjolnir user. Do not prefix them with sudo.

On Debian 12 or Ubuntu 22.04 and newer, install Podman and the rootless helper binaries with:

Terminal window
sudo apt update
sudo apt install -y podman uidmap slirp4netns ca-certificates

Older distribution releases can package Podman 3.x. The verification command below is authoritative: if it reports less than 4.0.0, upgrade to a currently supported Debian/Ubuntu release or install a distribution-supported Podman 4+ package before using Mjolnir.

Install Podman and the packages that provide rootless mapping and networking:

Terminal window
sudo dnf install -y podman shadow-utils slirp4netns ca-certificates

Rootless containers need a subordinate range for the Mjolnir user in both /etc/subuid and /etc/subgid. First inspect the current entries:

Terminal window
grep -E "^$(id -un):" /etc/subuid
grep -E "^$(id -un):" /etc/subgid

If either command has no output, an administrator must assign an unused range. For a new machine where 100000-165535 is not already allocated, this command creates a 65,536-ID range for the current user:

Terminal window
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"

Do not reuse that example range when it belongs to another account; choose a different non-overlapping range according to the host’s account policy. End the login session completely and sign in again after changing either file. Then run:

Terminal window
podman system migrate

Every command here is a postcondition. Resolve a failure before running Mjolnir. Run them by hand to diagnose a host; mj doctor --json --smoke checks sections 1 through 4 for every configured target and reports the same failures with an exact remediation.

Terminal window
podman --version

Expected: the reported version starts with 4. or a higher major version. For example, podman version 5.4.2 passes. A 3.x result fails; upgrade Podman as described above.

2. Rootless mode and subordinate mappings work

Section titled “2. Rootless mode and subordinate mappings work”
Terminal window
podman info --format '{{.Host.Security.Rootless}}'
grep -E "^$(id -un):" /etc/subuid
grep -E "^$(id -un):" /etc/subgid
podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map

Expected:

  • The first command prints exactly true.

  • Both grep commands print an entry for the current user.

  • Both map commands succeed. Their output must map container ID 0 and at least one additional ID (normally a first line mapping ID 0 followed by a line beginning at ID 1 with a large range). For example:

    0 1000 1
    1 100000 65536

Mjolnir runs the UID-map command itself before every local-Podman session. It also checks that container IDs 0 and 1 are mapped, which catches a login with no usable subordinate range.

3. The configured runtime image is available

Section titled “3. The configured runtime image is available”

For Mjolnir’s published image, set IMAGE to the exact image value from the Mjolnir target and pull it:

Terminal window
IMAGE=ghcr.io/brokkai/mjolnir/agent-dev:latest
podman pull "$IMAGE"
podman image exists "$IMAGE"

Both commands must exit zero. Replace the example with the configured image if it differs. For Mjolnir’s locally built localhost/mjolnir/agent-dev:latest, build it with the command above and verify it without attempting a registry pull:

Terminal window
podman image exists localhost/mjolnir/agent-dev:latest

4. A container can run, execute a command, and be removed

Section titled “4. A container can run, execute a command, and be removed”

Use the same image as the configured target. The Mjolnir development image supports the following verbatim:

Terminal window
IMAGE=localhost/mjolnir/agent-dev:latest
CHECK_NAME="mj-podman-check-$$"
podman run --init --detach --name "$CHECK_NAME" "$IMAGE" sleep infinity
podman exec "$CHECK_NAME" /bin/sh -c 'printf "Mjolnir Podman exec works\n"'
podman rm --force "$CHECK_NAME"

Expected: each command exits zero, the exec command prints Mjolnir Podman exec works, and podman container exists "$CHECK_NAME" exits nonzero after the remove command.

This independent check uses a small image with curl, so it does not assume curl is present in every custom runtime image:

Terminal window
podman run --rm docker.io/curlimages/curl:8.10.1 \
-fsSIL --max-time 15 https://github.com/ -o /dev/null

Expected: exit status zero. This confirms DNS, rootless container networking, CA certificates, and outbound HTTPS access needed to clone GitHub bundles.

Check the helpers:

Terminal window
command -v newuidmap
command -v newgidmap

If either command fails, install the package that provides them, then start a fresh login session and rerun verification:

Terminal window
sudo apt install -y uidmap
# or, on Fedora:
sudo dnf install -y shadow-utils

No /etc/subuid or /etc/subgid entry after the user was created

Section titled “No /etc/subuid or /etc/subgid entry after the user was created”

Create non-overlapping ranges for the Mjolnir user, then log out and log in:

Terminal window
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"
grep -E "^$(id -un):" /etc/subuid
grep -E "^$(id -un):" /etc/subgid
podman system migrate

If the example range is already assigned, the administrator must choose an unused range instead. Do not edit mappings for another account.

Mjolnir must run as an unprivileged user. Start a normal shell, do not invoke Mjolnir through sudo, and remove a remote/rootful Podman override before retrying:

Terminal window
unset CONTAINER_HOST
podman info --format '{{.Host.Security.Rootless}}'

If a named Podman connection is selected, switch back to the local rootless connection with the site’s normal podman system connection default policy.

Pulls or GitHub HTTPS fail inside a container

Section titled “Pulls or GitHub HTTPS fail inside a container”

First distinguish host DNS/TLS from container networking with the HTTPS command above. Install ca-certificates and slirp4netns using the distro commands if they are absent. Corporate proxies must be configured for the rootless Podman environment and passed to containers according to the organization’s policy; never put proxy credentials in Mjolnir’s committed configuration.

Use a current WSL2 distribution, not WSL1. On Windows, verify the version and update the WSL kernel when podman unshare reports operation not permitted or user namespaces are unavailable:

Terminal window
wsl -l -v
wsl --update
wsl --shutdown

Then reopen the Linux distribution, install Podman and uidmap inside that distribution, configure /etc/subuid and /etc/subgid there, and rerun every verification command. Mjolnir uses the Podman CLI directly and does not require a Podman API socket or a systemd service, but rootless networking still requires the WSL2 kernel support and slirp4netns (or the distro’s pasta setup). Keep Mjolnir repositories and Podman storage in the Linux filesystem (for example under ~/), not /mnt/c, for correct Linux permissions and substantially better overlay-filesystem performance.