Skip to main content

chroot-distro is a lightweight Linux container management utility built around chroot.

Project description

Chroot-Distro

PyPI Release PyPI Downloads License

Chroot-Distro is a utility for managing rootful Linux containers in Termux and on regular Linux hosts. It uses the host kernel's native chroot and bind mounts (mount --bind) to provide a high-performance, near-native Linux environment.

Containers are created by pulling Docker/OCI images directly from Docker Hub or any compatible registry — or by extracting a local tarball / OCI image archive. The container filesystem is assembled from the image layers and stored locally, ready to be entered at any time.

Chroot-Distro can also build OCI images from a Dockerfile (no Docker daemon required), storing the result in the local manifest cache or exporting it as a standalone OCI tarball.

Unlike proot-distro (which is rootless via proot), Chroot-Distro requires root privileges on the host. Mutating commands automatically re-launch themselves via sudo, doas, pkexec, or su when needed (see First-run check).


Table of contents

  1. Introduction
  2. Commands reference
  3. How Chroot-Distro works
  4. Storage layout
  5. Environment variables
  6. Shell completions
  7. Limitations
  8. Donate

Introduction

Chroot-Distro lets you run a full Linux userland — Ubuntu, Debian, Alpine, Arch, openSUSE, distroless server images, anything available as a Docker/OCI image — on top of Termux on a rooted Android device, or on top of a regular Linux distribution, with native kernel performance, without the overhead of proot's ptrace interception, and without a Docker daemon.

Typical use cases:

  • Running a desktop-class Linux distribution on a phone or tablet at near-native speed (rooted device required on Termux).
  • Disk-intensive and compile workloads (GCC/Clang, Rust, Go) without proot slowdowns.
  • Spinning up server software (Nginx, Nextcloud, PostgreSQL, etc.) on Android by reusing the same OCI images you'd run on a server.
  • Building custom OCI images from a Dockerfile on-device, without a Docker daemon — and pushing them to Docker Hub, GHCR, or any OCI-compatible registry.
  • Trying a distribution non-destructively: install, experiment, chroot-distro remove when done.

Installation

Chroot-Distro requires Python 3.10 or newer. There are no third-party Python dependencies. Because it uses native chroot and bind mounts, the effective user for mutating operations must be root (see First-run check).

On Termux (Android)

  1. Root your device (Magisk, KernelSU, APatch, or similar).
  2. Install Termux from F-Droid or Termux GitHub Releases.
  3. Install Chroot-Distro:
pkg install coreutils sudo python mount-utils
pip install chroot-distro

From a local checkout:

git clone https://github.com/sabamdarif/chroot-distro
cd chroot-distro
pip install .                     # regular install
# pip install -e .                # editable install for development

On a regular Linux host

# Debian/Ubuntu example:
sudo apt install python3-pip

pip install chroot-distro
# or from a checkout:
git clone https://github.com/sabamdarif/chroot-distro
cd chroot-distro
pip install .

First-run check

On startup, commands that modify containers or mounts verify that the effective UID is 0. If not, Chroot-Distro re-executes itself using, in order: sudo, doas, pkexec, or su.

Situation Behaviour
Default Auto-elevate when not root.
--no-elevate or CHROOT_DISTRO_NO_ELEVATE=1 Skip elevation; exit with an error if not root.
Termux, default Prefer su (real root) over sudo.
Termux, --use-sudo or CHROOT_DISTRO_USE_SUDO=1 Prefer sudo for elevation.

list, ps, search, and help do not require root and are never re-executed.

Quick start

# Install Ubuntu 24.04 from Docker Hub
chroot-distro install ubuntu:24.04

# Start a shell inside the container
chroot-distro login ubuntu

# Same thing, using the login alias
chroot-distro sh ubuntu

# Run a single command and exit
chroot-distro login ubuntu -- /bin/uname -a

# List all installed containers
chroot-distro list

# Build and install a custom image from a Dockerfile
chroot-distro build -t myapp:1.0 --install-as myapp ./mycontext

# Publish the built image to a registry
export CD_DOCKER_AUTH=myuser:mypassword
chroot-distro push myuser/myapp:1.0

# Rebuild from scratch (loses all in-container data)
chroot-distro reset ubuntu

# List only the containers that are currently running
chroot-distro ps

# Search Docker Hub for an image
chroot-distro search nextcloud

# See what changed in a container relative to its image
chroot-distro diff ubuntu

# Unmount bindings and end active sessions
chroot-distro unmount ubuntu

# Forcibly stop a running container (SIGKILL + unmount)
chroot-distro kill ubuntu

# Permanently remove a container (unmounts active sessions first)
chroot-distro remove ubuntu

Commands reference

Every subcommand supports --help (also -h), which prints help text laid out for the current terminal width.

Global flags (before the subcommand):

Option Description
-h, --help Show top-level help.
--no-elevate Do not auto-elevate to root (CHROOT_DISTRO_NO_ELEVATE=1).
--use-sudo On Termux, prefer sudo over su (CHROOT_DISTRO_USE_SUDO=1).

Short aliases are accepted for many commands (shlogin, rmremove, insinstall, etc.); each section below lists them.

install — Install a container

chroot-distro install [OPTIONS] (IMAGE or PATH or URL)
Aliases: add, i, in, ins

Pull a Docker/OCI image and create a container from it, extract a local archive, or download a remote archive over HTTP/HTTPS.

Options:

Option Description
-n, --name NAME Custom local container name. Defaults to the image name (without tag/registry) or the archive filename. Must start with a letter or digit; may contain only letters, digits, _, ., -.
--override-alias NAME Same as -n / --name (mutually exclusive).
-a, --architecture ARCH Override target CPU architecture. Accepts native names (aarch64, arm, i686, riscv64, x86_64) or Docker platform strings (linux/arm64, linux/amd64, …). Defaults to the host CPU.
-q, --quiet Suppress non-error output.

From a Docker/OCI registry

IMAGE is a standard Docker image reference:

Form Example
Official image ubuntu:24.04
Official, no tag (uses latest) alpine
User image myuser/myimage:tag
Custom registry ghcr.io/foo/bar:latest

Custom registries are detected when the first path component contains . or : (a hostname). Public images on ghcr.io, quay.io, registry.gitlab.com, etc. are pulled with an anonymous Bearer token discovered from each registry's /v2/ challenge.

Private images require credentials. Set CD_DOCKER_AUTH to username:password (or username:PAT) before running install. The colon separator is mandatory. PD_DOCKER_AUTH is accepted as a fallback for compatibility with proot-distro:

export CD_DOCKER_AUTH=myuser:mypassword
chroot-distro install myuser/private-image:tag

export CD_DOCKER_AUTH=myuser:ghp_xxx
chroot-distro install ghcr.io/myorg/private-image:tag

Layers are cached under $BASE_CACHE_DIR/oci_layers/ and reused on subsequent installs. If the resolved manifest and all layers are already cached, installation runs fully offline.

Missing layers are downloaded in parallel (default 4 workers). Set CD_DOWNLOAD_WORKERS to tune concurrency (integer 1–10; values above 10 are capped).

On Termux, list does not elevate privileges. If containers were installed as root, run once as root to fix legacy manifest permissions: su -c 'chmod -R a+r $PREFIX/var/lib/chroot-distro/containers/*/manifest.json' (or reinstall). New installs write manifest.json as world-readable (0644).

Examples:

chroot-distro install ubuntu:24.04
chroot-distro install alpine:3.21 --name my-alpine
chroot-distro install debian:bookworm --architecture aarch64
chroot-distro install ghcr.io/myorg/myimage:latest

From a local archive

IMAGE can be a path starting with /, ./, ../, or ~. A bare token like ubuntu is always treated as a Docker image reference.

Two archive formats are supported (auto-detected):

  • Plain rootfs tarball — top-level entries form a standard Linux filesystem (bin/, etc/, usr/, …). Strip level is scored automatically. Compression: gzip, bzip2, xz, lzma, or uncompressed. No manifest.json is written (reset and run are not available).
  • OCI image layout — archive contains oci-layout at its root (as from docker save or skopeo copy oci-archive:). Layers are applied with OCI whiteout semantics; manifest.json is written so reset and run work like registry installs.

Examples:

chroot-distro install ./alpine-rootfs.tar.gz
chroot-distro install ./myimage.oci.tar --name myimage

From a URL

When an HTTP or HTTPS URL is given instead of a local path, the archive is downloaded fully and then processed the same way as a local file. Only http:// and https:// are supported. The default container name is derived from the last URL path component; use --name to override.

chroot-distro install https://example.com/rootfs.tar.xz --name demo

After installation, if the image defines an Entrypoint, a Run entrypoint: chroot-distro run <name> hint is printed alongside Start shell: chroot-distro login <name>.


build — Build an image from a Dockerfile

chroot-distro build [OPTIONS] [PATH]

Build an OCI/Docker-compatible image from a Dockerfile. PATH is the build context directory (default .); all COPY/ADD source paths are resolved relative to it.

By default the built image is stored in the local manifest cache under the tag given by --tag (default <basename(PATH)>:latest). A subsequent chroot-distro install <tag> installs entirely without network access.

Options:

Option Description
-f, --file PATH Dockerfile at PATH instead of <PATH>/Dockerfile. Pass - to read from stdin.
-t, --tag REF Image reference to assign. Repeatable.
--build-arg K=V Set a build-time ARG (only declared ARGs are honoured). Repeatable.
--architecture ARCH Target CPU architecture (default: host).
--target STAGE Stop after the named multi-stage build stage.
-o, --output FILE Write an OCI image-layout tarball to FILE. Compression inferred from the extension. Repeatable.
--install-as NAME After build, install the image as container NAME.
--no-cache Disable per-step build caching.
-v, --verbose Echo each instruction and stream RUN output.
-q, --quiet Suppress non-error output.

Supported Dockerfile instructions:

FROM (multi-stage, FROM scratch, COPY --from=), RUN (shell, JSON exec, here-doc), COPY (--from, --chown, --chmod), ADD, CMD, ENTRYPOINT, ENV, ARG, LABEL, MAINTAINER, USER, WORKDIR, EXPOSE, VOLUME, STOPSIGNAL, HEALTHCHECK, SHELL, ONBUILD.

BuildKit-only features (RUN --mount, RUN --network, RUN --security, COPY --link, COPY --parents) are rejected with an explicit error.

chroot requirement:

If the Dockerfile contains any RUN instruction, each step executes inside the in-progress rootfs via chroot and therefore requires root. Metadata-only builds (COPY/ADD/ENV/… without RUN) run in pure-Python mode and do not require root.

Examples:

chroot-distro build .
chroot-distro build -t myapp:1.0 --install-as myapp .
chroot-distro build -t myapp:arm64 --architecture aarch64 -o myapp.oci.tar.gz .
chroot-distro build --build-arg HTTP_PROXY=$HTTP_PROXY -t myapp .

Limitations:

RUN steps run under chroot, not a real container runtime: no PID, network, or IPC isolation, no cgroups, no seccomp. Steps that depend on real namespaces or kernel features may fail or behave differently from docker build. Multi-platform manifest lists are not produced.


push — Push a built image to a registry

chroot-distro push [OPTIONS] IMAGE

Upload a locally built image to a Docker/OCI registry. The image must have been produced by chroot-distro build -t IMAGE first; push reads the manifest and blobs from the local cache. No Docker daemon is required.

Options:

Option Description
--architecture ARCH Push the manifest built for the given architecture (must match the build). Default: host.
-q, --quiet Suppress non-error output.

Authentication:

Set CD_DOCKER_AUTH=username:password (colon required). PD_DOCKER_AUTH is accepted as a fallback:

chroot-distro build -t myuser/myapp:1.0 ./mycontext
export CD_DOCKER_AUTH=myuser:mypassword
chroot-distro push myuser/myapp:1.0

Each layer is HEAD-probed first; existing blobs are skipped. 401/403 responses include a hint to set or fix CD_DOCKER_AUTH.


login — Start a shell inside a container

chroot-distro login [OPTIONS] CONTAINER [-- COMMAND ...]
Aliases: sh

Spawn an interactive shell (or a custom command) inside an installed container. The -- separator passes arguments to the container's login shell.

Examples:

chroot-distro login ubuntu
chroot-distro login ubuntu --user myuser
chroot-distro login ubuntu -- /bin/ls /etc
chroot-distro login ubuntu -- bash -c "echo hello"
chroot-distro sh ubuntu
chroot-distro login ubuntu --get-chroot-cmd

Options always available:

Option Description
-u, --user USER Log in as USER (default: root). Accepts name, numeric uid, name:group, or uid:gid.
--isolated Reduce host exposure and enable namespace isolation (mount, PID, UTS, IPC via unshare/nsenter). On Termux: also skip Android system, storage, and $PREFIX binds unless you opt in with --shared-* or --bind. (Fresh /tmp and /run are the default in every mode now, not just --isolated.) Mutually exclusive with --minimal.
--minimal Bare minimum chroot: core pseudo-filesystems only (/dev, /proc, /sys, plus /run, /dev/pts, /dev/shm when present). Stripped guest environment. Mutually exclusive with --isolated.
--shared-home Bind the invoking user's host home into the guest home (or /root for root). On Termux, binds TERMUX_HOME.
--shared-tmp Bind host tmp (/tmp on Linux, $PREFIX/tmp on Termux) to /tmp in the guest. Opt-in only: by default the container gets its own fresh /tmp, not the host's.
--shared-display Share the host display server (X11 and Wayland), audio (PulseAudio/PipeWire), and D-Bus session bus with the container. Binds only the specific session sockets, not the host's whole /run. Opt-in only. --shared-x11 is accepted as a backward-compatible alias.
-b, --bind SRC[:DST] Bind-mount a custom host path (repeatable). DST must be an absolute guest path.
--hostname STRING Hostname inside the container (default: the container name).
-w, --work-dir PATH Initial working directory (default: user's home).
-e, --env VAR=VALUE Set a guest environment variable (repeatable).
--get-chroot-cmd Print the fully assembled env + chroot command line and exit.

Display sharing

Display sharing is active when using --shared-display (or --shared-x11 as a backward-compatible alias).

It work best on regular Linux and in Termux it doesn't have all the options

By default the container is isolated from the host's runtime state: its /tmp and /run are fresh and empty, so host temp files and host runtime sockets (D-Bus, PulseAudio, etc.) do not leak in. /proc, /sys, /dev, and /dev/pts are still bound from the host for hardware/USB access. --shared-display is the only way to expose the GUI/audio/D-Bus sockets; it binds only the specific sockets needed, never the whole /run.

Display sharing forwards four subsystems from the invoking host session into the container:

X11

  • /tmp/.X11-unix socket directory is bind-mounted into the guest.
  • DISPLAY, XAUTHORITY, and XDG_RUNTIME_DIR are forwarded.
  • The X authority file is bind-mounted when it lives outside /run.
  • Compositors such as niri with xwayland-satellite often authenticate X11 clients by Unix-socket UID. Chroot-distro aligns the guest user's UID to the invoking host user when needed. If the guest cannot read the cookie file, it is copied to /var/tmp/.chroot-distro-xauthority (requires xauth on the host). If that fails, use --shared-home, xhost +SI:localuser:GUEST, or a UID-matched user.

Wayland

  • WAYLAND_DISPLAY is forwarded (fallback: wayland-0 if socket exists).
  • XDG_SESSION_TYPE, XDG_CURRENT_DESKTOP, and DESKTOP_SESSION are forwarded from the host session.
  • The host's XDG_RUNTIME_DIR (/run/user/<uid>) is bind-mounted whole with rslave propagation, so every session socket (Wayland, PulseAudio, PipeWire, D-Bus) is exposed and sockets created after mount stay visible — while the host's broad /run stays hidden. The system D-Bus socket (/run/dbus/system_bus_socket) is bound individually.

Note: logging in as root with --shared-display cannot use the session D-Bus bus (it rejects uid 0 with "Connection reset by peer"); log in as a UID-matched normal user with --user for a working session bus. The system bus works for root.

Sound (PulseAudio / PipeWire)

  • PULSE_SERVER is forwarded (fallback: unix:/run/user/<uid>/pulse/native if the PulseAudio socket exists).
  • PipeWire apps discover their socket automatically via XDG_RUNTIME_DIR; no extra env var is needed.

D-Bus

  • DBUS_SESSION_BUS_ADDRESS is forwarded (fallback: unix:path=/run/user/<uid>/bus if the socket exists).

Use --isolated to skip all display sharing, or --minimal for only core pseudo-filesystems. Home is never bind-mounted unless you pass --shared-home.

GPU acceleration (auto-detected)

Chroot-distro automatically enables hardware-accelerated GPU rendering at login — no flag needed, same tier as USB and general hardware access. GPU passthrough is independent of --shared-display. (It doesn't work on Termux.)

AMD and Intel (open-source Mesa drivers)

  • Works out of the box. Host /dev (including /dev/dri/ render nodes) is bind-mounted into the container. The host's Vulkan/EGL/OpenCL ICD and loader-config descriptors are bound read-only so the guest's own Mesa stack can enumerate the hardware. Driver .so files are not bound: shadowing the container's own Mesa libraries corrupts its loader.

NVIDIA — native Linux (proprietary driver)

  • Detection: /dev/nvidia0 exists, or libcuda*.so* / libnvidia*.so* found under /usr/lib*/.
  • Bind-mounts: /dev/nvidia* device nodes, /dev/dri/card* and /dev/dri/renderD* DRM nodes, host NVIDIA .so libraries mapped to the correct guest library directory (multi-arch aware), NVIDIA config and ICD files (/etc/, EGL/Vulkan JSON descriptors, OpenCL ICD), and NVIDIA CLI tools (nvidia-smi, etc.). Vendor-neutral GLVND/GBM dispatch libraries (libGL, libEGL, libGLX, libgbm, …) and zero-byte sources are never bound, so the container's own loader is not shadowed.
  • Runs independent of --shared-display: the GPU works whether or not the display is shared.
  • Environment variables set: __NV_PRIME_RENDER_OFFLOAD=1, __GLX_VENDOR_LIBRARY_NAME=nvidia.
  • Guest ldconfig is run inside the chroot to refresh the shared library cache after the new libraries are bind-mounted.

Namespace isolation (--isolated)

With --isolated, chroot-distro creates a per-container namespace holder (unshare) and runs bind mounts, special mounts, and chroot inside that environment (nsenter). Supported namespaces: mount, PID, UTS, and IPC. Isolation is all-or-nothing: chroot-distro probes the full requested namespace set first, and if any one of them is unsupported on the kernel it acquires none of them and falls back fully to a non-isolated login (with a warning naming the missing namespace), so a session is never left half-isolated. This is inspired by Ubuntu-Chroot and is not a full container runtime: there is no network namespace, no user namespace mapping, and no image layering.

Do not mix --isolated and non-isolated logins on the same container without running chroot-distro unmount <name> first. Concurrent --isolated sessions share the same holder and mounts.

Host bindings (Termux, default mode)

Without --isolated or --minimal, the following host paths are bind-mounted when present and readable:

/apex
/data/app
/data/dalvik-cache
/data/misc/apexdata/com.android.art/dalvik-cache
/data/data/<termux-app-package>
/linkerconfig/com.android.art/ld.config.txt
/linkerconfig/ld.config.txt
/odm
/plat_property_contexts
/product
/property_contexts
/sdcard
/storage/emulated/0
/storage/self/primary
/system
/system_ext
/vendor

For normal-type containers, the Termux $PREFIX is also bound at its original path so Termux utilities (termux-api, pkg, etc.) remain reachable inside the guest.

Guest environment

The host environment is not carried into the guest. Precedence (later entries win):

  1. Baseline: PATH (from DEFAULT_PATH_ENV), MOZ_FAKE_NO_SANDBOX=1, PULSE_SERVER=127.0.0.1 (Termux only).
  2. Image-defined Env from manifest.json (display and GPU vars are blocked here — they are always set by auto-detection).
  3. Android system vars (ANDROID_*, BOOTCLASSPATH, …), Termux only, when not --isolated and not --minimal.
  4. Your --env VAR=VALUE entries.
  5. HOME, USER, TERM (default xterm-256color), COLORTERM (when set on the host), and HOSTNAME (the --hostname value, else the container name).
  6. When display sharing is active (via --shared-display): DISPLAY, XAUTHORITY, XDG_RUNTIME_DIR, WAYLAND_DISPLAY, XDG_SESSION_TYPE, XDG_CURRENT_DESKTOP, DESKTOP_SESSION, PULSE_SERVER, DBUS_SESSION_BUS_ADDRESS. Your --env entries override these.
  7. On Linux (unless --minimal): GPU env vars when NVIDIA is auto-detected (independent of --shared-display): native — __NV_PRIME_RENDER_OFFLOAD, __GLX_VENDOR_LIBRARY_NAME; WSL2 — GALLIUM_DRIVER, MESA_D3D12_DEFAULT_DEVICE_TYPE, LIBGL_ALWAYS_SOFTWARE. Your --env entries override these.

HOSTNAME is always set to the container name (or --hostname). The hostname/uname commands report it only under --isolated, where the UTS namespace is given a real hostname; without --isolated they still report the host's name (no UTS namespace to change).

On Termux (unless isolated or minimal), $PREFIX/bin is appended to PATH. A snippet at /etc/profile.d/termux-profile.sh re-applies login-time variables after the distro's /etc/profile runs, so su - someuser inside the container does not drop them.

In --minimal mode only your --env entries plus TERM/COLORTERM are exported.

Session lifecycle

The first login or run for a container performs bind mounts and increments a session counter. Each exiting session decrements it; when the counter reaches zero, all bind mounts are unmounted automatically. Use unmount to force teardown (see below).


run — Run the image-defined entrypoint

chroot-distro run [OPTIONS] CONTAINER [-- ARG ...]

Run the Entrypoint and/or Cmd from the container's OCI manifest (equivalent to docker run). Requires an OCI install with manifest.json (plain tarball installs have no recorded Entrypoint/Cmd).

Entrypoint and Cmd resolution:

Image Args after -- Inner command
Entrypoint + Cmd (none) Entrypoint + Cmd
Entrypoint + Cmd ARGS Entrypoint + ARGS (Cmd replaced)
Only Cmd (none) Cmd
Only Cmd ARGS ARGS (Cmd replaced)
Only Entrypoint (none) Entrypoint
Only Entrypoint ARGS Entrypoint + ARGS
Neither (none) Error
Neither ARGS ARGS

When --work-dir is not given, run uses the image WorkingDir (falling back to /).

run accepts the same options as login. See chroot-distro login --help.

Examples:

chroot-distro run hello-world
chroot-distro run ubuntu -- /bin/echo hi
chroot-distro run nextcloud --get-chroot-cmd

list — List installed containers

chroot-distro list [OPTIONS]
Aliases: li, ls

Show installed containers (subdirectories of containers/ with a rootfs/). For each container prints rootfs size, image source (Docker/OCI reference and architecture from manifest.json, or local archive for plain tarballs), and status (idle or in use with PID when another command holds the container lock). Does not require root. When none are installed, an install suggestion is printed.

Option Description
-q, --quiet Print only container names, one per line.

ps — List running containers

chroot-distro ps [OPTIONS]

List only containers that are currently running — those with a live process inside their chroot or an active namespace holder. Columns match list (rootfs size, image source, status). Does not require root.

Option Description
-a, --all Show all installed containers, not just running ones.
-q, --quiet Print only container names, one per line.

search — Search Docker Hub

chroot-distro search [OPTIONS] TERM
Aliases: find, se

Search Docker Hub for images matching TERM and print the image name, star count, whether it is an official image, and a short description. Uses a single unauthenticated request to the Docker Hub search API. Requires network access; does not require root.

Option Description
-l, --limit N Maximum number of results to show (default 25, max 100).

Examples:

chroot-distro search nextcloud
chroot-distro search --limit 50 ubuntu

diff — Inspect filesystem changes

chroot-distro diff CONTAINER

Inspect changes to files and directories in a container's filesystem relative to the OCI/Docker image it was installed from (like docker diff). The image baseline is reconstructed from the cached OCI layers, then compared against the live rootfs. Output uses Docker-style markers:

Marker Meaning
A File or directory was added
C File or directory was changed
D File or directory was deleted

Pseudo-filesystem mount points (/dev, /proc, /sys, /run, /tmp) are excluded. Available only for containers installed from an image whose layers are still present in the cache (avoid clear-cache to keep diff working).


remove — Delete a container

chroot-distro remove [OPTIONS] CONTAINER
Aliases: rm

Permanently delete the container and all its data. This cannot be undone and is not confirmed.

Before deletion, active mounts are detected via /proc/mounts and unmounted cleanly. File permissions are fixed on the fly so chmod-000'd subtrees can always be removed.

Option Description
-v, --verbose Log each deleted file.
-q, --quiet Suppress non-error output. Mutually exclusive with --verbose.

unmount — Unmount a container

chroot-distro unmount CONTAINER
Aliases: umount, um

Safely unmount a container's bind mounts. If chroot processes are still running, SIGTERM is sent (with SIGKILL after two seconds if needed), the session counter is reset to 0, and all bind mounts are removed. If a path is busy, a lazy unmount (umount -l) is attempted as a fallback.


kill — Forcibly stop a running container

chroot-distro kill CONTAINER
Aliases: k, stop

Forcibly stop a running container: all processes inside its chroot are sent SIGTERM and then SIGKILL after a short grace period, the bind mounts are unmounted, and the namespace holder (if any) is released. This is the abrupt counterpart to unmount (equivalent to docker kill).


rename — Rename a container

chroot-distro rename OLDNAME NEWNAME

Rename a container from OLDNAME to NEWNAME.

Option Description
-q, --quiet Suppress non-error output.

reset — Reinstall a container from scratch

chroot-distro reset CONTAINER

Remove the container rootfs and reinstall from the image recorded in containers/<name>/manifest.json. All data inside the container is lost. Requires an OCI install (plain rootfs tarballs cannot be re-pulled).

Option Description
-q, --quiet Suppress non-error output.

backup — Archive a container

chroot-distro backup [OPTIONS] CONTAINER
Aliases: bak, bkp

Create a TAR archive containing <name>/manifest.json (when present) and <name>/rootfs/.

Options:

Option Description
-o, --output FILE Write to FILE instead of stdout. Refuses to overwrite an existing file.
-c, --compress TYPE Force compression: gzip, bzip2, xz, or none.
-v, --verbose Log each archived file.
-q, --quiet Suppress non-error output. Mutually exclusive with --verbose.

Compression is inferred from the file extension unless --compress overrides it. Without --output, the archive goes to stdout (uncompressed by default); stdout cannot be a TTY.

File ownership in the archive is zeroed. Block/char devices, FIFOs, and sockets are skipped. backup is TTY-safe when piping into interactive tools (e.g. gpg -c).

Examples:

chroot-distro backup ubuntu --output ubuntu.tar.xz
chroot-distro backup ubuntu | gzip > ubuntu.tar.gz
chroot-distro backup ubuntu | gpg -c > ubuntu.tar.gpg

restore — Restore a container from a backup

chroot-distro restore [OPTIONS] [BACKUP_FILE]

Restore from a TAR archive. When BACKUP_FILE is omitted, data is read from stdin. Compression is auto-detected.

Options:

Option Description
-v, --verbose Log each extracted file.
-q, --quiet Suppress non-error output. Mutually exclusive with --verbose.

Archive format requirements:

  • Files must live under <name>/manifest.json and <name>/rootfs/…. Bare-root archives are rejected.
  • Without manifest.json, login still works but reset and run will not.
  • Hard links in the archive are materialised as independent copies.

restore is TTY-safe for interactive pipelines (gpg -d archive.gpg | chroot-distro restore).

Examples:

chroot-distro restore ubuntu.tar.xz
cat ubuntu.tar.xz | chroot-distro restore
gpg -d ubuntu.tar.gpg | chroot-distro restore

copy — Copy files to or from a container

chroot-distro copy [OPTIONS] [CONTAINER:]SRC [CONTAINER:]DEST
Aliases: cp

Copy files between the host and a container rootfs, or between two containers. In-container paths use the container:path prefix.

Option Description
-r, --recursive Copy directories recursively (preserves symlinks).
-m, --move Move instead of copy (delete source after success).
-v, --verbose Log each copied file.
-q, --quiet Suppress non-error output. Mutually exclusive with --verbose.

Examples:

chroot-distro copy ./file.txt ubuntu:/root/file.txt
chroot-distro copy ubuntu:/etc/resolv.conf ./resolv.conf.bak
chroot-distro copy --recursive ./myapp ubuntu:/opt/myapp

sync — Synchronize files to or from a container

chroot-distro sync [OPTIONS] [CONTAINER:]SRC [CONTAINER:]DEST

Synchronize SRC to DEST, copying only files that differ. Always recursive.

Comparison method:

Mode What is compared
Default File size and integer modification time
--checksum File size and CRC32 checksum

Regular files are written atomically (.~cd_sync temp file → os.replace). Symlinks are copied as-is. Hard links become independent copies. Block/char devices, FIFOs, and sockets are skipped.

Option Description
-c, --checksum Compare by size + CRC32 instead of size + mtime.
-d, --delete Remove destination entries with no source counterpart.
-v, --verbose Log each synced or deleted entry.
-q, --quiet Suppress non-error output. Mutually exclusive with --verbose.

Examples:

chroot-distro sync ./app ubuntu:/opt/app
chroot-distro sync --checksum ./data ubuntu:/data
chroot-distro sync --delete ./app ubuntu:/opt/app

clear-cache — Delete the download cache

chroot-distro clear-cache
Aliases: clear, cl

Remove every entry from $BASE_CACHE_DIR — layer blobs (oci_layers/), resolved manifests (oci_manifests/), and the build cache index (build_cache_index.json). Freed disk space is reported in human-readable units.

Option Description
-v, --verbose Log each deleted file.
-q, --quiet Suppress non-error output. Mutually exclusive with --verbose.

After clear-cache, the next install or reset of an image requires network access again.


help — Show command help

chroot-distro help [COMMAND]
Aliases: h, he, hel

Print detailed help for COMMAND, or general usage when omitted.


How Chroot-Distro works

Chroot-Distro is a thin orchestration layer around two primary building blocks:

1. OCI registry client

The install command speaks the OCI Distribution protocol directly over urllib:

  • Public images on Docker Hub need no credentials (e.g. ubuntu:24.04).
  • Public images on other registries use a full reference (e.g. ghcr.io/myorg/myimage:tag).
  • Manifest lists are resolved to the platform matching your CPU (or --architecture).
  • Each layer blob is downloaded with SHA-256 verified before entering the cache.
  • Layer blobs and the resolved single-arch manifest are cached locally.

Layers are applied in order with full OCI whiteout semantics. After all layers are applied, Chroot-Distro adds small fixups when /etc/ exists:

  • /etc/resolv.conf is replaced with Google DNS (8.8.8.8 / 8.8.4.4).
  • /etc/hosts gets a minimal localhost mapping.
  • On Termux, the host Android user is registered as aid_<name> in /etc/passwd, /etc/group, etc., so Android UID permissions work inside the guest.

The OCI manifest and image config are saved to containers/<name>/manifest.json for reset and run.

Local archives and HTTP/HTTPS URLs follow the same extraction paths as in the install section.

2. Native chroot and bind mounts

Unlike proot, which rewrites paths via ptrace, Chroot-Distro uses real kernel features:

  • Bind mounts (mount --bind) for host directories inside the guest.
  • Session tracking under $RUNTIME_DIR/data/<name>/sessions.
  • Automatic mount/unmount: the first session mounts; the last session exiting unmounts everything.
  • Lazy unmount fallback (umount -l) when a target is busy.

A typical login invocation looks roughly like:

env PATH= HOME=/root USER=root  \
  chroot /…/containers/ubuntu/rootfs \
  /bin/sh -c 'cd /root && exec /bin/bash -l'

Add --get-chroot-cmd to print the exact command line without running it.

Cross-architecture support

Guest architectures (aarch64, arm, i686, x86_64, riscv64) are detected at login by reading ELF headers of common shell binaries. Cross-arch execution uses QEMU user-mode via binfmt_misc / QEMU user binaries installed on the host.


Storage layout

All runtime data lives under $RUNTIME_DIR:

  • Termux: $TERMUX__PREFIX/var/lib/chroot-distro/, where TERMUX__PREFIX defaults to /data/data/com.termux/files/usr.
  • Regular Linux: $XDG_DATA_HOME/chroot-distro/ (default ~/.local/share/chroot-distro/).

The OCI cache ($BASE_CACHE_DIR) is under $RUNTIME_DIR/cache on Termux, and under $XDG_CACHE_HOME/chroot-distro/ (default ~/.cache/chroot-distro/) on a regular Linux host.

Because mutating commands run as root after auto-elevation, effective paths on Linux are typically under /root/.local/share/ and /root/.cache/ unless you set XDG_DATA_HOME / XDG_CACHE_HOME.

Path Contents
containers/<name>/rootfs/ Container root filesystem
containers/<name>/manifest.json Image reference, arch, OCI manifest, image config
data/<name>/sessions Active login / run session counter
locks/<name>.lock Per-container POSIX flock
locks/build/<key>.lock Build/push lock
$BASE_CACHE_DIR/oci_layers/ Cached OCI layer blobs
$BASE_CACHE_DIR/oci_manifests/ Cached single-arch manifests
$BASE_CACHE_DIR/build_cache_index.json Dockerfile build cache index

Environment variables

User-configurable variables

Variable Effect
TERMUX__PREFIX Override Termux prefix; drives RUNTIME_DIR on Termux. Default: /data/data/com.termux/files/usr.
TERMUX__HOME Override Termux home for --shared-home bindings. Default: /data/data/com.termux/files/home.
TERMUX_APP__PACKAGE_NAME Termux app package (default com.termux); used for /data/data/<pkg>/… binds.
TERMUX_APP__APP_VERSION_NAME, TERMUX_VERSION Either counts toward Termux detection when set.
XDG_DATA_HOME Base for $XDG_DATA_HOME/chroot-distro/ on non-Termux hosts. Default: ~/.local/share.
XDG_CACHE_HOME Base for $XDG_CACHE_HOME/chroot-distro/ on non-Termux hosts. Default: ~/.cache.
CD_DOCKER_AUTH Registry credentials as username:password or username:PAT (colon required). Used by install, build (FROM pulls), and push. PD_DOCKER_AUTH is accepted as a fallback.
CD_DOWNLOAD_WORKERS Parallel registry layer downloads during install (default 4, maximum 10). Invalid values use the default; out-of-range values are clamped.
CD_DOWNLOAD_RATE_LIMIT Bandwidth limit for downloads (e.g., 5M for 5 MiB/s, default 0 = unlimited). Supports suffixes K, M, G (case-insensitive).
CD_DOWNLOAD_MAX_RETRIES Maximum retry attempts per connection failure (default 3, clamped between 0 and 20).
CD_FORCE_NO_COLORS When set, disables ANSI colours in Chroot-Distro output.
CHROOT_DISTRO_NO_ELEVATE When set to 1, disables privilege auto-elevation (same as --no-elevate).
CHROOT_DISTRO_USE_SUDO When set to 1, prefer sudo over su on Termux (same as --use-sudo).
COLUMNS Fallback terminal width for --help rendering.
TERM, COLORTERM Inherited into the guest (always; even in --minimal). TERM defaults to xterm-256color when unset on the host.

Auto-set guest environment variables

These are set automatically by chroot-distro at login. They cannot be overridden from manifest.json image Env, but can be overridden with --env.

Display and audio (Linux, non-minimal, display sharing active):

Variable Source / Fallback
DISPLAY Host $DISPLAY; fallback :0
XAUTHORITY Host $XAUTHORITY; fallback ~/.Xauthority; fallback Xwayland auth file in /run/user/<uid>/
XDG_RUNTIME_DIR Host $XDG_RUNTIME_DIR; fallback /run/user/<uid>
WAYLAND_DISPLAY Host $WAYLAND_DISPLAY; fallback wayland-0 if socket exists
XDG_SESSION_TYPE Host $XDG_SESSION_TYPE (no fallback)
XDG_CURRENT_DESKTOP Host $XDG_CURRENT_DESKTOP (no fallback)
DESKTOP_SESSION Host $DESKTOP_SESSION (no fallback)
PULSE_SERVER Host $PULSE_SERVER; fallback unix:/run/user/<uid>/pulse/native if socket exists
DBUS_SESSION_BUS_ADDRESS Host $DBUS_SESSION_BUS_ADDRESS; fallback unix:path=/run/user/<uid>/bus if socket exists

Hostname (always set, non-minimal):

Variable Source / Fallback
HOSTNAME --hostname value; fallback the container name. Under --isolated the UTS namespace hostname is also set so hostname/uname -n report it.

GPU — NVIDIA native Linux (auto-detected, non-minimal):

Variable Value
__NV_PRIME_RENDER_OFFLOAD 1
__GLX_VENDOR_LIBRARY_NAME nvidia

GPU — WSL2 with NVIDIA (auto-detected, non-minimal):

Variable Value
GALLIUM_DRIVER d3d12
MESA_D3D12_DEFAULT_DEVICE_TYPE GPU
LIBGL_ALWAYS_SOFTWARE 0

Shell completions

Completion scripts for Bash, Zsh, and Fish live in src/chroot_distro/completions/:

  • chroot-distro.bash
  • _chroot-distro
  • chroot-distro.fish

They complete subcommands, global flags (--no-elevate, --use-sudo), and per-command options (including login/run flags such as --shared-home, --shared-display, --get-chroot-cmd, --isolated, and --minimal).

If your shell does not pick them up automatically, install them manually:

# Bash
mkdir -p ~/.local/share/bash-completion/completions
cp src/chroot_distro/completions/chroot-distro.bash \
   ~/.local/share/bash-completion/completions/chroot-distro

# Zsh (add fpath before compinit in ~/.zshrc)
mkdir -p ~/.zsh/completions
cp src/chroot_distro/completions/_chroot-distro ~/.zsh/completions/_chroot-distro

# Fish
mkdir -p ~/.config/fish/completions
cp src/chroot_distro/completions/chroot-distro.fish \
   ~/.config/fish/completions/chroot-distro.fish

Limitations

Kernel and chroot limitations

  • Root required: real chroot and bind mounts need appropriate privileges; there is no rootless mode.
  • No real init: systemd, socket-activated supervisors, and full init systems generally do not work. Individual long-running processes are fine.
  • Kernel features: FUSE modules, real iptables, custom cgroup hierarchies, and similar kernel-module features may not work inside the guest.
  • Namespaces: --isolated provides mount/PID/UTS/IPC isolation via unshare/nsenter, but there is no network namespace and no parity with Docker or Podman.
  • Bind mount hygiene: crashed sessions or orphan processes can leave mounts busy; unmount and lazy unmount mitigate this but orphaned processes should be cleaned up.

Chroot-Distro limitations

  • Termux requires root: unlike proot-distro, Chroot-Distro cannot run containers on a non-rooted Android device.
  • Registry authentication: private pulls and pushes need CD_DOCKER_AUTH=user:password (or PD_DOCKER_AUTH). Docker config.json credential helpers are not read.
  • Dockerfile builds are not BuildKit: RUN executes under chroot, not a real container runtime. BuildKit-only Dockerfile features are rejected. Multi-platform manifest lists are not produced — build and push once per architecture.
  • push is single-arch: no manifest-list assembly, cross-repo blob mounting, or chunked uploads.
  • No live state migration: backup/restore capture the rootfs and manifest, not in-memory process state.

Donate

If this project is useful to you, tips in cryptocurrency are welcome:

Bitcoin

13Q7xf3qZ9xH81rS2gev8N4vD92L9wYiKH

Ethereum / USDT (BEP20, ERC20)

0x1d216cf986d95491a479ffe5415dff18dded7e71

USDT (TRC20)

TCjRKPLG4BgNdHibt2yeAwgaBZVB4JoPaD

Dogecoin

DJkMCnBAFG14TV3BqZKmbbjD8Pi1zKLLG6

Issues and contributing

Acknowledgments

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

chroot_distro-2.2.0.tar.gz (325.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chroot_distro-2.2.0-py3-none-any.whl (240.6 kB view details)

Uploaded Python 3

File details

Details for the file chroot_distro-2.2.0.tar.gz.

File metadata

  • Download URL: chroot_distro-2.2.0.tar.gz
  • Upload date:
  • Size: 325.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for chroot_distro-2.2.0.tar.gz
Algorithm Hash digest
SHA256 dded23857939c7d19e71596019c620b34edc899600362725cbbe4b3b12cb6254
MD5 bd69dd9cb96f962fb375f84fb298c273
BLAKE2b-256 cbed50993da241b36c9f9879fa2e5daf31a88a8df5d1a8458931c21895532ea0

See more details on using hashes here.

Provenance

The following attestation bundles were made for chroot_distro-2.2.0.tar.gz:

Publisher: publish.yml on sabamdarif/chroot-distro

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file chroot_distro-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: chroot_distro-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 240.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for chroot_distro-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6dbeea39088fd8433a7a223c3b560c51fc143011ea32fca727163cac723bc491
MD5 5c2ca6f35f09e95a8aef524dc7558936
BLAKE2b-256 f68880d07cd200243e3f52edf8cf03f34749d00f4064c01d6230cbe8f89750b8

See more details on using hashes here.

Provenance

The following attestation bundles were made for chroot_distro-2.2.0-py3-none-any.whl:

Publisher: publish.yml on sabamdarif/chroot-distro

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page