# ecr - ephemeral chroot ![](example.gif) Instantly drop into a disposable Linux environment on your host machine. No Docker daemon. No VM. No root. Just namespaces. ```sh 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: ```toml [dependencies] ecr = { git = "https://github.com/…" } # or a path / registry source ``` ```rust 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: ```rust 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] [-- 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 ` | Overlay-mount a directory read-only inside the chroot (default: current directory at `/root/`) | | `--bind-rw ` | Bind-mount a directory read-write at `/mnt/` | | `--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 ` | Target architecture (`amd64`, `arm64`, `armhf`, `riscv64`, …) | | `--kernel[=PATH]` | Boot with QEMU system emulation. Downloads Alpine's default kernel if no `=PATH` given (`linux-virt`, or `linux-lts` on riscv64 where the virt flavor is not built) | | `-m, --memory ` | Memory for QEMU VM (default: 2G, only with `--kernel`) | ## Examples ```sh # 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: ```sh # Auto-download Alpine's default 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 default kernel: `linux-virt`, falling back to `linux-lts` on riscv64 where the virt flavor is not built; the downloaded image is stored decompressed so QEMU can load it) - 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-` 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` / `newgidmap` in `$PATH` - An entry in `/etc/subuid` and `/etc/subgid` for your user - For foreign-arch: QEMU user-mode emulation registered with `binfmt_misc`