README gains a Library section covering prepare, the exec options (envp, bind targets, arch) and the provisioned-rootfs hot-cell flow. SPEC documents the workspace layout, the library API, the cache sidecars (.digest, .provisioned) and the updated execution flow. AGENTS.md scope paths follow the new crates/ layout.
ecr - ephemeral chroot
Instantly drop into a disposable Linux environment on your host machine.
No Docker daemon. No VM. No root. Just namespaces.
ecr debian # interactive Debian shell
ecr alpine:3.23 # specific Alpine version
ecr ubuntu:noble # Ubuntu by codename
ecr fedora # any Docker Hub image
How it works
ecr pulls a root filesystem (Alpine/Ubuntu direct from their CDNs; everything else from Docker Hub), extracts it into a temporary directory, and execs a shell inside a user + mount + PID + UTS namespace. The process tree is isolated, the rootfs is discarded on exit, and your host is never touched.
Library
ecr is a cargo workspace: crates/ecr is the library crate (package ecr), crates/ecr-cli the thin command-line front-end. Rust consumers can use the library directly — resolve an image, download it through the content cache, and run commands with a fully caller-controlled environment, bind targets and architecture:
[dependencies]
ecr = { git = "https://github.com/…" } # or a path / registry source
use ecr::{chroot, BindTarget, ExecOptions, PrepareRequest, RootfsCache};
// Download alpine through the cache (~/.cache/ecr) and extract to a scratch dir
let cache = RootfsCache::new(RootfsCache::default_dir()?);
let rootfs = cache.prepare(&PrepareRequest::new("alpine:3.23"))?;
// Caller-composed envp, explicit bind targets, target architecture
let code = rootfs.exec(&ExecOptions {
arch: "amd64".into(), // empty = host arch
binds: vec![BindTarget { // host dir -> in-rootfs mount
source: "./data".into(),
target: "/mnt/data".into(),
read_only: false, // true = overlay (ro)
}],
env: chroot::default_env(rootfs.dir()), // or your own envp
command: vec!["cat".into(), "/etc/os-release".into()],
..ExecOptions::default()
})?;
rootfs.persist()?; // write the rootfs back into the cache (see below)
Provisioned-rootfs caching (hot cells)
Prepare once, provision once, amortize every later run — including runs from other processes sharing the same cache directory:
let rootfs = cache.prepare_provisioned(&PrepareRequest::new("alpine:3.23"), |rootfs| {
// runs at most once per cache entry
rootfs.exec(&ExecOptions {
command: vec!["apk".into(), "add".into(), "curl".into()],
env: chroot::default_env(rootfs.dir()),
..ExecOptions::default()
})?;
Ok(())
})?;
On the first call the pristine image is downloaded, the closure provisions it, and the result is persisted into the cache with a .provisioned marker. On every subsequent call the entry is hit directly: no download, no provisioning. Note a provisioned entry no longer tracks the upstream image; delete the entry (or pass no_cache: true) to re-provision.
Usage
ecr [OPTIONS] <DISTRO[:VERSION]> [-- COMMAND...]
| Argument | Description |
|---|---|
ubuntu, alpine, debian, fedora, … |
Distribution to enter |
:version |
Optional version tag — codename, number, or latest / lts / edge |
-- cmd arg… |
Run a command instead of an interactive shell |
Options
| Flag | Description |
|---|---|
--bind <PATH> |
Overlay-mount a directory read-only inside the chroot (default: current directory at /root/<name>) |
--bind-rw <PATH> |
Bind-mount a directory read-write at /mnt/<name> |
--no-bind |
Skip all directory mounts |
--no-cache |
Force a fresh download, bypassing the cache |
-v, --verbose |
Print diagnostic output (URLs, layer info, extraction steps) |
-a, --arch <ARCH> |
Target architecture (amd64, arm64, armhf, riscv64, …) |
--kernel[=PATH] |
Boot with QEMU system emulation. Downloads Alpine's linux-virt kernel if no =PATH given |
-m, --memory <SIZE> |
Memory for QEMU VM (default: 2G, only with --kernel) |
Examples
# Quick shell in the latest Debian
ecr debian
# Run a one-shot command
ecr alpine -- sh -c 'apk add curl && curl -s https://example.com'
# Work on your project inside Ubuntu — changes are visible on the host
ecr ubuntu --bind-rw ~/projects/myapp
# Native-compile for ARM64 through emulation (requires QEMU binfmt_misc)
ecr --arch arm64 alpine -- uname -m
# Always pull a fresh image
ecr --no-cache fedora
# Boot with QEMU system emulation (auto-downloads default kernel)
ecr --kernel alpine
# Boot with your own kernel (note the `=` — a space would parse the path as the distro)
ecr --kernel=/boot/vmlinuz ubuntu
# Boot with custom memory
ecr --kernel --memory 4G alpine
QEMU System Mode
When --kernel is specified, ecr boots the rootfs in a full QEMU virtual machine instead of using namespaces:
# Auto-download Alpine's linux-virt kernel (recommended)
ecr --kernel alpine
# Use your own kernel
ecr --kernel=/boot/vmlinuz ubuntu
This mode:
- Creates an uncompressed CPIO initramfs from the rootfs (streamed to disk)
- Boots QEMU with your kernel (or auto-downloads Alpine's
linux-virtkernel) - Provides full VM isolation
- Works for any architecture (no binfmt_misc needed)
- Caches the default kernel in
~/.cache/ecr/
Host bind mounts (--bind, --bind-rw) are not applied in this mode.
Requirements:
qemu-system-<arch>installed- For custom kernels: kernel must have serial console support
Supported distributions
| Name | Source | Version examples |
|---|---|---|
alpine |
Alpine CDN | latest, stable, edge, 3.23, 3.22 |
ubuntu |
Ubuntu CDN | latest, lts, noble, 24.04 |
debian fedora arch gentoo |
Docker Hub | latest, any tag |
| Any Docker Hub / OCI image | Registry | org/image:tag, ghcr.io/…, localhost:5000/… |
Cache
Downloaded images are cached in ~/.cache/ecr/. For latest OCI tags the registry manifest digest is checked on each run — the image is only re-downloaded when it has actually changed. Use --no-cache to force a fresh pull regardless. The library's PreparedRootfs::persist can replace a cache entry with a provisioned rootfs (see Provisioned-rootfs caching above).
Requirements
- Linux kernel ≥ 5.1
- Unprivileged user namespaces enabled (
/proc/sys/kernel/unprivileged_userns_clone= 1 on some distros) newuidmap/newgidmapin$PATH- An entry in
/etc/subuidand/etc/subgidfor your user - For foreign-arch: QEMU user-mode emulation registered with
binfmt_misc
