chroot-distro is a lightweight Linux container management utility built around chroot.
Project description
Chroot-Distro
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
- Introduction
- Commands reference
- How Chroot-Distro works
- Storage layout
- Environment variables
- Shell completions
- Limitations
- 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
prootslowdowns. - 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 removewhen 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)
- Root your device (Magisk, KernelSU, APatch, or similar).
- Install Termux from F-Droid or Termux GitHub Releases.
- 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 (sh → login, rm →
remove, ins → install, 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. Nomanifest.jsonis written (resetandrunare not available). - OCI image layout — archive contains
oci-layoutat its root (as fromdocker saveorskopeo copy oci-archive:). Layers are applied with OCI whiteout semantics;manifest.jsonis written soresetandrunwork 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-unixsocket directory is bind-mounted into the guest.DISPLAY,XAUTHORITY, andXDG_RUNTIME_DIRare 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(requiresxauthon the host). If that fails, use--shared-home,xhost +SI:localuser:GUEST, or a UID-matched user.
Wayland
WAYLAND_DISPLAYis forwarded (fallback:wayland-0if socket exists).XDG_SESSION_TYPE,XDG_CURRENT_DESKTOP, andDESKTOP_SESSIONare 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/runstays hidden. The system D-Bus socket (/run/dbus/system_bus_socket) is bound individually.
Note: logging in as root with
--shared-displaycannot use the session D-Bus bus (it rejects uid 0 with "Connection reset by peer"); log in as a UID-matched normal user with--userfor a working session bus. The system bus works for root.
Sound (PulseAudio / PipeWire)
PULSE_SERVERis forwarded (fallback:unix:/run/user/<uid>/pulse/nativeif 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_ADDRESSis forwarded (fallback:unix:path=/run/user/<uid>/busif 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.sofiles are not bound: shadowing the container's own Mesa libraries corrupts its loader.
NVIDIA — native Linux (proprietary driver)
- Detection:
/dev/nvidia0exists, orlibcuda*.so*/libnvidia*.so*found under/usr/lib*/. - Bind-mounts:
/dev/nvidia*device nodes,/dev/dri/card*and/dev/dri/renderD*DRM nodes, host NVIDIA.solibraries 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
ldconfigis 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):
- Baseline:
PATH(fromDEFAULT_PATH_ENV),MOZ_FAKE_NO_SANDBOX=1,PULSE_SERVER=127.0.0.1(Termux only). - Image-defined
Envfrommanifest.json(display and GPU vars are blocked here — they are always set by auto-detection). - Android system vars (
ANDROID_*,BOOTCLASSPATH, …), Termux only, when not--isolatedand not--minimal. - Your
--env VAR=VALUEentries. HOME,USER,TERM(defaultxterm-256color),COLORTERM(when set on the host), andHOSTNAME(the--hostnamevalue, else the container name).- 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--enventries override these. - 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--enventries 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.jsonand<name>/rootfs/…. Bare-root archives are rejected. - Without
manifest.json, login still works butresetandrunwill 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.confis replaced with Google DNS (8.8.8.8 / 8.8.4.4)./etc/hostsgets 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/, whereTERMUX__PREFIXdefaults 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-distrochroot-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
chrootand 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:
--isolatedprovides mount/PID/UTS/IPC isolation viaunshare/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;
unmountand 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(orPD_DOCKER_AUTH). Dockerconfig.jsoncredential helpers are not read. - Dockerfile builds are not BuildKit:
RUNexecutes underchroot, not a real container runtime. BuildKit-only Dockerfile features are rejected. Multi-platform manifest lists are not produced — build and push once per architecture. pushis single-arch: no manifest-list assembly, cross-repo blob mounting, or chunked uploads.- No live state migration:
backup/restorecapture 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
- Bug reports: https://github.com/sabamdarif/chroot-distro/issues
- License: GPL-3.0-only. See LICENSE.
Acknowledgments
- proot-distro — architecture and CLI design inspiration.
- pyLoad — sliding-window speed tracking, token-bucket rate limiting, and connection resilience algorithms.
- distrobox - shared-display option improvements
- Magisk-Modules-Alt-Repo/chroot-distro
- ravindu644/Ubuntu-Chroot
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chroot_distro-2.2.1.tar.gz.
File metadata
- Download URL: chroot_distro-2.2.1.tar.gz
- Upload date:
- Size: 327.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8365abf959dc3f6baad9f3e6f37600d8f296a12400cbd0b773dd79847e788cac
|
|
| MD5 |
a75cfa22764caae528325d54d0e32608
|
|
| BLAKE2b-256 |
87cf2e69da9f4f7b899a14f4854831639485c6fde999107435e00a90cb3ccea3
|
Provenance
The following attestation bundles were made for chroot_distro-2.2.1.tar.gz:
Publisher:
publish.yml on sabamdarif/chroot-distro
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chroot_distro-2.2.1.tar.gz -
Subject digest:
8365abf959dc3f6baad9f3e6f37600d8f296a12400cbd0b773dd79847e788cac - Sigstore transparency entry: 1861068160
- Sigstore integration time:
-
Permalink:
sabamdarif/chroot-distro@359795134b5a543a27d28b8a226495be767b20f8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sabamdarif
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@359795134b5a543a27d28b8a226495be767b20f8 -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file chroot_distro-2.2.1-py3-none-any.whl.
File metadata
- Download URL: chroot_distro-2.2.1-py3-none-any.whl
- Upload date:
- Size: 241.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
446986d5f66bab3abea51c3104e3b4154be84fcad1a2c78997d6908e4c3b9d8a
|
|
| MD5 |
a45051f128b1eefd4497bc880a8f27dc
|
|
| BLAKE2b-256 |
1abc838a7e394e50e4670978cab544f5258c786fcb952af6de27a849425e2509
|
Provenance
The following attestation bundles were made for chroot_distro-2.2.1-py3-none-any.whl:
Publisher:
publish.yml on sabamdarif/chroot-distro
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chroot_distro-2.2.1-py3-none-any.whl -
Subject digest:
446986d5f66bab3abea51c3104e3b4154be84fcad1a2c78997d6908e4c3b9d8a - Sigstore transparency entry: 1861068291
- Sigstore integration time:
-
Permalink:
sabamdarif/chroot-distro@359795134b5a543a27d28b8a226495be767b20f8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sabamdarif
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@359795134b5a543a27d28b8a226495be767b20f8 -
Trigger Event:
workflow_run
-
Statement type: