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.
How Mjolnir uses Podman
Section titled “How Mjolnir uses Podman”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:
podman --versionpodman info --format '{{.Host.Security.Rootless}}'podman unshare cat /proc/self/uid_mapFor 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.
Optional host-managed workspace storage
Section titled “Optional host-managed workspace storage”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.
Git clone cache
Section titled “Git clone cache”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:
podman pull ghcr.io/brokkai/mjolnir/agent-dev:latestBuilding it locally remains a supported alternative, for example to customize the image or to work offline:
podman build --pull=always \ --file containers/Containerfile.agent-dev \ --tag localhost/mjolnir/agent-dev:latest \ containersInstall Podman rootlessly
Section titled “Install Podman rootlessly”Run all podman commands below as the normal Mjolnir user. Do not prefix them with
sudo.
Debian and Ubuntu
Section titled “Debian and Ubuntu”On Debian 12 or Ubuntu 22.04 and newer, install Podman and the rootless helper binaries with:
sudo apt updatesudo apt install -y podman uidmap slirp4netns ca-certificatesOlder 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.
Fedora
Section titled “Fedora”Install Podman and the packages that provide rootless mapping and networking:
sudo dnf install -y podman shadow-utils slirp4netns ca-certificatesAssign subordinate UID and GID ranges
Section titled “Assign subordinate UID and GID ranges”Rootless containers need a subordinate range for the Mjolnir user in both
/etc/subuid and /etc/subgid. First inspect the current entries:
grep -E "^$(id -un):" /etc/subuidgrep -E "^$(id -un):" /etc/subgidIf 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:
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:
podman system migrateVerification
Section titled “Verification”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.
1. Podman is installed and supported
Section titled “1. Podman is installed and supported”podman --versionExpected: 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”podman info --format '{{.Host.Security.Rootless}}'grep -E "^$(id -un):" /etc/subuidgrep -E "^$(id -un):" /etc/subgidpodman unshare cat /proc/self/uid_mappodman unshare cat /proc/self/gid_mapExpected:
-
The first command prints exactly
true. -
Both
grepcommands print an entry for the current user. -
Both map commands succeed. Their output must map container ID
0and at least one additional ID (normally a first line mapping ID0followed by a line beginning at ID1with a large range). For example:0 1000 11 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:
IMAGE=ghcr.io/brokkai/mjolnir/agent-dev:latestpodman 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:
podman image exists localhost/mjolnir/agent-dev:latest4. 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:
IMAGE=localhost/mjolnir/agent-dev:latestCHECK_NAME="mj-podman-check-$$"podman run --init --detach --name "$CHECK_NAME" "$IMAGE" sleep infinitypodman 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.
5. Containers can reach GitHub over HTTPS
Section titled “5. Containers can reach GitHub over HTTPS”This independent check uses a small image with curl, so it does not assume curl is present in every custom runtime image:
podman run --rm docker.io/curlimages/curl:8.10.1 \ -fsSIL --max-time 15 https://github.com/ -o /dev/nullExpected: exit status zero. This confirms DNS, rootless container networking, CA certificates, and outbound HTTPS access needed to clone GitHub bundles.
Common failures and exact remediations
Section titled “Common failures and exact remediations”newuidmap or newgidmap is missing
Section titled “newuidmap or newgidmap is missing”Check the helpers:
command -v newuidmapcommand -v newgidmapIf either command fails, install the package that provides them, then start a fresh login session and rerun verification:
sudo apt install -y uidmap# or, on Fedora:sudo dnf install -y shadow-utilsNo /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:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"grep -E "^$(id -un):" /etc/subuidgrep -E "^$(id -un):" /etc/subgidpodman system migrateIf the example range is already assigned, the administrator must choose an unused range instead. Do not edit mappings for another account.
Rootless check prints false
Section titled “Rootless check prints false”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:
unset CONTAINER_HOSTpodman 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:
wsl -l -vwsl --updatewsl --shutdownThen 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.