xcrunner
A rootless container runtime for Docker images. It runs the images that coala and coala-runtime use on hosts without Docker and without root.
Two engines, chosen automatically:
- ns: kernel user namespaces plus overlayfs, no helper binary. Native speed. Needs a Linux kernel 5.11 or newer with unprivileged user namespaces enabled. Most workstations, cloud VMs, and current HPC nodes qualify.
- proot: a vendored static PRoot runs the container under ptrace. Works on any Linux, slower, and creation copies the image rootfs.
Install
pip install xcrunner # or: uv pip install xcrunner
xcrunner info # shows the engine and probe results
Optional: pip install 'xcrunner[zstd]' for zstd-compressed layers.
On aarch64 hosts without user namespaces, provide a PRoot binary with
XCODON_PROOT=/path/to/proot. The command is xcrunner. The package on PyPI is xcrunner, and the Python module is xcodon_runtime.
Use
xcrunner pull python:3.12-slim
xcrunner run --rm -v $PWD:/work -w /work python:3.12-slim python -c 'print("hi")'
xcrunner create --name dev python:3.12-slim
xcrunner start dev
xcrunner exec dev pip install numpy # persists in the container's writable layer
xcrunner exec dev python -c 'import numpy'
xcrunner stop dev && xcrunner rm dev
xcrunner logs dev # the ns engine's keeper log
xcrunner rmi python:3.12-slim # drop a tag, and the image with its last tag
xcrunner prune # leftovers: half-built dirs and orphan blobs
xcrunner prune --all # also unused layers and day-old exited containers
xcrunner run --rm --env-dir $PWD/.xcrunner-env python:3.12-slim pip install numpy
xcrunner run --rm --env-dir $PWD/.xcrunner-env python:3.12-slim python -c 'import numpy'
--env-dir keeps the container's writable layer in a host folder, keyed by
image id, so tools installed in one container are there for the next one.
Delete <env-dir>/<image-id> to reset. coala-runtime uses this through the
XCRUNNER_ENV_DIR variable.
Two global flags come before the subcommand: --engine ns|proot forces an
engine, and --home DIR picks the state directory for this one command.
Images already in a local Docker daemon are reused through docker save, so
locally built images work without a registry. xcrunner records the daemon's
image id at import. When the daemon's tag later points at another image
(for example after a docker build there), the next run, create, or
build that uses the tag imports it again. --pull never and image ids
skip this check.
Build and commit
xcrunner build -t myapp:1 . # runs a Dockerfile subset
xcrunner commit -m "installed numpy" dev myapp:2 # snapshot a stopped container
xcrunner tag myapp:2 myapp:latest
xcrunner build runs each Dockerfile instruction in its own container and
commits one image per step, so unchanged steps are cached. The cache lives
in <home>/build-cache.json. The step images are untagged, and
xcrunner prune --all removes untagged images, which empties the cache in
effect. xcrunner commit also works on an --env-dir folder instead of a
container: xcrunner commit --env-dir $PWD/.xcrunner-env --image myapp:1 myapp:2.
Docker-style names work too:
xcrunner image inspect myapp:1 # also: image ls, image rm
xcrunner docker build -t myapp:1 . # any docker verb xcrunner supports
xcrunner docker image inspect --format '{{.Id}}' myapp:1
xcrunner docker VERB ... takes docker's own verbs and flags for build,
image inspect|ls|rm, images, rmi, tag, pull, run, create,
start, exec, stop, rm, ps, logs, commit, inspect, version, and
info. Other verbs exit with code 125 and a message.
For tools that shell out to a real docker binary directly — running
docker build or docker image inspect as a subprocess, rather than going
through cwltool's --user-space-docker-cmd — xcrunner shim install writes
a docker script that forwards every call to xcrunner docker:
xcrunner shim install --dir ~/.local/bin
export PATH="$HOME/.local/bin:$PATH"
docker build -t myapp:1 .
docker image inspect myapp:1
The shim refuses to install while any real docker is anywhere on PATH,
even outside DIR, unless you pass --force. It also refuses to replace a
DIR/docker that is not an xcrunner shim unless you pass --force, and it
never replaces a directory. xcrunner also never mistakes its own shim for a
real Docker daemon: image pulls that would reuse a local docker save skip
a shim.
Limits on the Dockerfile subset: no multi-stage builds (a second FROM, or
--from=), no .dockerignore, and no remote ADD/COPY from a URL.
Tools without conda
Agents often install command-line tools with conda create, conda install and
conda run. On a host with no conda, xcrunner can answer those calls itself:
xcrunner shim install conda --dir ~/.xcodon/shim
export PATH="$HOME/.xcodon/shim:$PATH"
This downloads a pinned micromamba (2.9.0, from conda-forge, checksum-verified) to
<xcrunner home>/bin/micromamba-2.9.0/micromamba and writes conda, mamba and
micromamba scripts that forward to xcrunner conda.
Offline, pass --micromamba PATH to use a binary you already have.
- Environments live in the project's
.xcrunner-env/conda(found from the working directory, or fromXCRUNNER_ENV_DIR), else under the xcrunner home. Downloads are cached once under the xcrunner home. A.xcrunner-envfound by searching upward is used only when you own it and no one else can write to it. - Launchers should export
XCRUNNER_ENV_DIR=<project>/.xcrunner-env, or create<project>/.xcrunner-env, before the agent starts. Otherwise an early conda call falls back to the xcrunner home: its envs land outside the project,conda env listin the project does not show them, and the agent'srun_shellguardrail refuses their paths. - A relative
-p PATHis always a folder under the working directory, even without a/. - Your own
~/.condaand~/.condarcare never read or written. conda run -n NAME CMDruns CMD with the env on PATH;conda activateis not supported, because it changes the calling shell.- Each env gets
conda-explicit.txt, listing every package URL and checksum, so it can be rebuilt withconda create -p PATH --file conda-explicit.txt. - The shim refuses to install while a real conda, mamba or micromamba is on PATH,
unless you pass
--force. - The shims use the default xcrunner home, or
XCODON_RUNTIME_HOMEwhen it is set; they do not remember anxcrunner --home Hgiven at install time. Afterxcrunner --home H shim install conda ..., also exportXCODON_RUNTIME_HOME=Hwherever the shims run, or they will look for micromamba in the default home instead.
Sandbox activation
xcrunner sandbox activate /abs/project/.xcrunner-env writes docker, conda,
mamba and micromamba shims into the project's .xcrunner-env/bin and prints
the two export lines (PATH first, then XCRUNNER_ENV_DIR) that send a shell's
docker and conda calls to xcrunner. The Python API is
xcodon_runtime.sandbox.activate(env_dir). opencodon calls it by itself when
its container engine is xcrunner, so --container-engine xcrunner is enough.
Environment record
xcrunner keeps .xcrunner-env/environment.json, a per-project record of the images a
project used (id, source, registry digests, and any build Dockerfile) and the
packages each install added, changed or removed.
For each image it also keeps the platform, package counts, and for built images
the packages the build added. .xcrunner-env/packages/<image id>.json holds the full
package list of each image the project used, not only the changes.
The record updates automatically after conda changes and when containers stop.
xcrunner env show prints it, xcrunner env show --json prints the file, and
xcrunner env record rebuilds it from what is on disk. Images keep their entries as
history; env record never removes one.
It is a record, not a restore mechanism: it does not recreate images or packages, only describes what happened. Images built before this feature have no Dockerfile on record. On the proot engine each container stop rescans the rootfs copy, which takes about 3-4 s on large images.
Hosts without docker
On a host with no docker daemon, a FROM name that only ever existed in a
local daemon, such as coala-runtime-python:latest, is looked up on Docker
Hub and not found there. Seed such base images once: pull the published
image and give it the local name.
xcrunner pull hubentu/coala-runtime-python:latest
xcrunner tag hubentu/coala-runtime-python:latest coala-runtime-python:latest
xcrunner pull hubentu/coala-runtime-r:latest
xcrunner tag hubentu/coala-runtime-r:latest coala-runtime-r:latest
After that, FROM coala-runtime-python:latest uses the stored image and
never goes to the network. xcrunner does not map image names itself.
Clusters with shared storage
Overlayfs cannot write its upper layer to NFS, Lustre or GPFS. When the home is on shared storage, the overlay probe fails and xcrunner falls back to the slower proot engine. Keep the image store on shared storage and put container folders on a node-local disk:
export XCODON_RUNTIME_HOME=/shared/$USER/xcrunner
export XCRUNNER_CONTAINER_DIR=${TMPDIR:-/tmp}/xcrunner-containers
Images are pulled once into the shared home and read from there. Each container's
writable layer, its keeper log and its locks live under XCRUNNER_CONTAINER_DIR.
Containers are then local to one node: xcrunner ps on another node does not list
them. xcrunner info shows both folders and the engine the probes chose.
The setting does not move an env folder's layers. On the ns engine they must also be
on a local disk. Set XCRUNNER_ENV_LAYER_DIR to node-local storage: the record stays
in the project's env folder, and each node keeps its own layers for as long as that
disk lasts. Or set XCODON_ENGINE=proot to keep the layers on shared storage.
macOS
On an Apple silicon Mac with macOS 26 or newer, xcrunner runs the Linux xcrunner inside one Lima VM. Install Lima first:
brew install lima
pip install xcrunner
xcrunner machine start # the first start creates the VM and takes a few minutes
xcrunner run --rm alpine echo hi
- The VM is a Lima
vzVM namedxcrunnerwith Ubuntu 24.04 and Rosetta. Images, layers and containers live on its own disk. - Your home folder,
/private/var/foldersand/private/tmpare shared into the VM at the same paths, so bind mounts and cwltool's temp folders work unchanged. Work in one of those folders, or add more withXCRUNNER_MACHINE_MOUNTSbefore the VM is created. conda,shim,sandboxandmachinerun on the Mac. Every other command runs in the VM. The conda shim installs macOS programs with a macOS micromamba.- Env folder layers live on the VM disk; the record in
.xcrunner-envstays with the project.xcrunner infoshows where the layers are. - x86_64-only images, such as most biocontainers, run through Rosetta.
xcrunner machine status|stop|shell|rmmanage the VM.rmdeletes its disk, with all images and env layers.
scripts/mac_smoke.sh runs the end-to-end checks on a Mac and writes a report to
~/xcrunner-mac-smoke.txt.
Environment variables
| Variable | Meaning |
|---|---|
XCODON_RUNTIME_HOME |
State directory. Default ~/.xcodon/runtime. |
XCRUNNER_CONTAINER_DIR |
Container folders. Default <home>/containers. |
XCRUNNER_ENV_LAYER_DIR |
Env folder layers on another disk. Default: in the env folder. On a cluster, a node-local disk gives each node its own layers. |
XCRUNNER_MACHINE_NAME |
macOS: the Lima VM's name. Default xcrunner. |
XCRUNNER_MACHINE_CPUS, XCRUNNER_MACHINE_MEMORY, XCRUNNER_MACHINE_DISK |
macOS: VM size when it is created. Defaults 4, 4GiB, 100GiB. |
XCRUNNER_MACHINE_MOUNTS |
macOS: extra Mac folders to share, separated by :. |
XCODON_ENGINE |
ns or proot. Skips probing. |
XCODON_PROOT |
Path to a PRoot binary. |
XCODON_PROOT_ARGS |
Extra PRoot flags, for example -k 5.15.0. |
XCODON_LOG |
info or debug. |
Limits
- One uid inside the container. Every image file is owned by that uid.
chownto another user fails. Setuid binaries do not elevate. - Host network only. No
--net=none, no port mapping. - No cgroups.
--memoryand--cpusare accepted and ignored with a warning. - No GPU passthrough.
- macOS: Apple silicon and macOS 26 or newer only, through a Lima VM.
- Read-only binds apply to the top mount only; submounts under a bound host path stay writable.
coala
In coala, add one branch to configure_container_runner:
if container_runner == "xcrunner":
runtime_context.user_space_docker_cmd = shutil.which("xcrunner") or "xcrunner"
cwltool then calls xcrunner inspect, xcrunner pull, and xcrunner run with
docker-style flags.
coala-runtime
xcodon-runtime ships xcodon_runtime.coala_adapter.XcodonContainerManager,
which implements coala-runtime's ContainerManager interface. In
coala-runtime, add XCRUNNER = "xcrunner" to ContainerEngine, return the
adapter from make_container_manager for that value, and try it in
autodetection after Docker and Podman and before Apptainer.
Development
uv venv .venv && . .venv/bin/activate
uv pip install -e ".[dev,zstd]"
python scripts/fetch_proot.py # only if _bin/ is missing
pytest -q # unit + ns + proot on a capable host
XCODON_TEST_NETWORK=1 pytest -m network
Metadata
Release files for xcrunner 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xcrunner-0.1.3.tar.gz | 975.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xcrunner-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.9 MB
Release files / xcrunner-0.1.3.tar.gz
| Download URL | xcrunner-0.1.3.tar.gz |
|---|---|
| Size | 975.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b07642b1bb299bc9e5b75903e5427e53852e8c07d8d3b9b72f2fc3b85ff3e332
|
|
BLAKE2b-256 checksum How to use checksums |
d49c8d035fa613d3101f0fbc012c4707f28784e022daf5c245524bebc20c4e80
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / xcrunner-0.1.3-py3-none-any.whl
| Download URL | xcrunner-0.1.3-py3-none-any.whl |
|---|---|
| Size | 901.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4213575f5664f1ffef426981c0ef0b47253bd272419666f01b5172f59292118a
|
|
BLAKE2b-256 checksum How to use checksums |
6c8c53423b878e58f63b26ac1d4d8e0ec6cafdb17adeb23db99f9c15bf6dc241
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log