Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rootd-fs

CI License: MIT Python

Rootless container filesystems for Termux and Linux.

rootd-fs pulls OCI/Docker images and turns them into container filesystems you can enter as a shell — no root, no daemon, no namespaces required. It speaks the registry protocol directly and hands execution to either PRoot or udocker.

rootd install ubuntu --autostart

That pulls Ubuntu 24.04, prepares it, and makes every new terminal open inside it.


Install

git clone https://github.com/ivansslo/rootd-fs
cd rootd-fs
./install.sh

With the udocker back-end as well:

ROOTD_WITH_UDOCKER=1 ./install.sh

Requirements: Python 3.9+ and proot. The installer handles both on Termux and Debian-family systems. There are no Python dependencies.


Usage

rootd install <preset|image>   pull an image and create a box
rootd enter [box]              open a shell inside it
rootd sh [box] -- <cmd>        run one command
rootd svc [box]                run a preset's service in the foreground
rootd ls                       list boxes
rootd info [box]               show details
rootd rm <box>                 delete a box
rootd rename <old> <new>       rename a box
rootd default [box]            show or set the default box
rootd autostart on|off|status  manage the login-shell hook
rootd backup [box]             write a box to an archive
rootd restore <archive>        recreate a box from an archive
rootd completion <shell>       print or install tab completion
rootd docker [box] --host      point a Docker client at a remote daemon
rootd tailscale [box] <action> manage Tailscale inside a box
rootd ssh [box] -- user@host   SSH out from inside a box
rootd caps                     show kernel capabilities and limits
rootd purge                    remove every box, cache and shell hook
rootd login <registry>         store registry credentials
rootd logout <registry>        remove them
rootd logins                   list stored logins (never secrets)
rootd presets                  list curated images
rootd doctor                   check the host environment
rootd prune                    clear the layer cache

A box is one container filesystem plus a small JSON record. Boxes live under $PREFIX/var/lib/rootd-fs on Termux and $XDG_DATA_HOME/rootd-fs elsewhere.

Examples

rootd install ubuntu                     # preset
rootd install nextcloud                  # preset with a service
rootd install termux-docker              # Termux userland image
rootd install ghcr.io/owner/img:tag       # any registry
rootd install alpine --name build --arch arm64

rootd enter ubuntu
rootd sh ubuntu -- apt update
rootd svc nextcloud                      # then open http://localhost:8080
rootd enter ubuntu --dry-run             # print the command, run nothing

Presets

Preset Image Notes
ubuntu ubuntu:24.04 general purpose
debian debian:bookworm small and stable
alpine alpine:latest ~8 MB, musl
arch archlinux:latest amd64 only upstream
fedora fedora:latest recent toolchains
void void-glibc:latest from ghcr.io
termux-docker termux/termux-docker Termux prefix layout
docker docker:cli client for a remote daemon
tailscale tailscale/tailscale mesh VPN, userspace mode
netshell alpine ssh, mosh, rsync, curl toolbox
nextcloud nextcloud:stable-apache service on :80
postgres postgres:16-alpine service on :5432
nginx nginx:alpine service on :80
python python:3.12-slim dev image
node node:22-slim dev image

Any name that is not a preset is treated as a plain image reference, so the whole of Docker Hub, GHCR, Quay and private registries are reachable.


Autostart

Installing a box does not change your shell. After rootd install ubuntu you still get your normal Termux prompt. Auto-entry is opt-in — silently hijacking your shell would be a good way to lock you out of your own phone.

To opt in, either install with the flag or enable it afterwards:

rootd install ubuntu --autostart    # at install time
rootd autostart on ubuntu           # or later, for an existing box
rootd autostart status
rootd autostart off

Once enabled, every new Termux session and tab opens inside the box — Termux's default bash sources ~/.bashrc for interactive shells, so no extra configuration is needed.

It appends a marked block to .bashrc, .zshrc, or config.fish and saves a .rootd-bak copy first. The block is written to fail safe:

  • it only runs in interactive shells, so scripts, scp and rsync are unaffected;
  • it refuses to run when ROOTD_BOX is already set, so it cannot recurse;
  • ROOTD_NO_AUTO=1 skips it without editing any file;
  • if the box is missing or its engine is gone, it prints a note and leaves you on the host shell rather than a dead terminal.

If you ever need to recover:

ROOTD_NO_AUTO=1 bash
rootd autostart off

Private registries

echo "$GITHUB_PAT" | rootd login ghcr.io -u ivansslo --password-stdin
rootd install ghcr.io/ivansslo/private-app:latest

Credentials are checked against the registry before being saved, so a bad token fails at login rather than at your next pull. They are stored in auth.json at mode 0600 using Docker's format, and an existing ~/.docker/config.json is read automatically — if you have already run docker login, pulls may just work.

rootd logins                 # registries and usernames, never secrets
rootd logout ghcr.io
rootd install --anonymous …  # ignore stored credentials for one pull

Use --password-stdin. A secret passed as --password lands in your shell history and in the process list, and rootd-fs will warn you about it. Note also that Docker's storage format is base64 — that is encoding, not encryption; the file mode is the real protection.

Do you need to configure GitHub secrets for this repository? No. Tests, builds, releases and GHCR pushes all use the automatic GITHUB_TOKEN. PyPI publishing uses Trusted Publishing (OIDC) with no stored token. See docs/PUBLISHING.md.


Docker: client, not daemon

A Docker daemon cannot run on a stock Android kernel. This is not a rootd-fs limitation and no configuration works around it. dockerd needs real uid 0, a writable cgroup hierarchy, overlayfs, and veth pairs. PRoot only fakes uid 0 — it rewrites syscall arguments, it does not grant kernel privilege. Run rootd caps to see exactly which features your kernel is missing.

What works, and works well, is the Docker client pointed at a machine that can run containers:

rootd install docker
rootd docker docker --host ssh://opc@your-oci-server
rootd sh docker -- docker ps
rootd sh docker -- docker compose up -d

This is not a compromise — the Docker CLI has always been a REST client over a socket, and DOCKER_HOST is the supported way to aim it somewhere else. You get the full command set against your server's real daemon, with its real cgroups and real overlayfs.

Combined with Tailscale, your server needs no public port at all:

rootd tailscale tailscale daemon        # terminal 1
rootd tailscale tailscale up --ssh      # terminal 2
rootd docker docker --host ssh://opc@100.x.y.z   # tailnet address

If you genuinely need a local daemon on the phone, the only real option is a full VM with its own kernel (QEMU), which is out of scope here.


Tailscale

rootd install tailscale
rootd tailscale tailscale daemon                 # foreground daemon
rootd tailscale tailscale up --ssh               # authenticate
rootd tailscale tailscale ip                     # show tailnet address
rootd tailscale tailscale status

On Android /dev/net/tun is not usable from an unprivileged process, so the daemon runs in userspace networking mode. That means:

  • the node is reachable from your tailnet (so your OCI server can SSH into the phone);
  • traffic originating in the box can egress through the SOCKS5 proxy on localhost:1055;
  • it does not create a phone-wide VPN — other Android apps keep using the normal network.

--ssh enables Tailscale SSH, which is the simplest way to reach the phone from your server without managing keys.


SSH

rootd install netshell
rootd ssh netshell --keygen                      # generate a key in the box
rootd ssh netshell --save opc@your-oci-server    # remember a default
rootd ssh netshell                               # connect
rootd ssh netshell -L 8080:localhost:80 -- opc@server

Keeping the client inside a box means keys, known_hosts and config live in one place that rootd backup captures.


Engines

proot (default) udocker
Mechanism ptrace syscall rewriting PRoot / Fakechroot / rootless runc
Works on stock Android yes mostly (P-modes)
Speed moderate F-modes are much faster
Root emulation yes P and R modes only
rootd install alpine --engine udocker --mode F1

udocker modes: P1 P2 (PRoot), F1F4 (Fakechroot), R1R3 (rootless runc/crun), S1 (Singularity). On Android, unprivileged user namespaces are usually disabled and seccomp filtering is often broken, so P2 is the default there; P1 is the default on Linux.

udocker's own storage is redirected under the rootd-fs state directory, so uninstalling removes everything.


Backup and restore

rootd backup ubuntu                      # -> ubuntu-20260727-1430.tar.xz
rootd backup ubuntu -o /sdcard/ub.tar.xz --compress gz
rootd restore ub.tar.xz                  # original name from the manifest
rootd restore ub.tar.xz --name staging   # or a new one

Archives are plain tars, so tar tf works on them. Each carries a rootd-backup.json manifest recording the image, architecture, engine, preset, mounts, and environment, so a restored box comes back configured rather than bare.

Restore runs through the same traversal-hardened extractor as image layers: a crafted archive cannot write outside the box directory. A restore that fails part way removes the partial box rather than leaving a broken one.


Tab completion

rootd completion bash --install
rootd completion zsh --install
rootd completion fish --install

Without --install the script is printed to stdout. Completion covers subcommands, flags, engine and mode values, and completes box names and preset names dynamically from your actual install.


Environment

Variable Effect
ROOTD_NO_AUTO=1 skip the login-shell hook
ROOTD_STATE_DIR where boxes are stored
ROOTD_CACHE_DIR where layers are cached
ROOTD_NO_SECCOMP=1 disable PRoot seccomp (slower, fixes some kernels)
ROOTD_UDOCKER_MODE default udocker mode
ROOTD_PROOT_BIN use a specific proot binary
NO_COLOR disable colour

Uninstalling / purge

rootd-fs keeps everything under one tree, so removal is complete and predictable. On Termux that tree is $PREFIX/var/lib/rootd-fs; elsewhere it follows XDG.

rootd purge          # inventory, confirm, then delete

It prints exactly what will go, with sizes, and requires you to type purge to proceed. Then remove the program itself:

pip uninstall rootd-fs

Options:

rootd purge --keep-cache   # keep downloaded layers for a reinstall
rootd purge -y             # no prompt (scripts)
rootd prune                # only the layer cache, keep the boxes
rootd rm <box>             # only one box

Back up first if you care about anything inside a boxrootd backup <box> writes an archive that survives the purge, as long as you write it somewhere outside the state tree (your home directory or /sdcard).

Doing it by hand on Termux

If rootd itself is broken and you just want the disk space back:

# 1. the data (this is the big one)
rm -rf $PREFIX/var/lib/rootd-fs

# 2. the launcher and the package
rm -f $PREFIX/bin/rootd $PREFIX/bin/rootd-fs
pip uninstall rootd-fs

# 3. the completion files
rm -f $PREFIX/share/bash-completion/completions/rootd
rm -f $PREFIX/share/zsh/site-functions/_rootd
rm -f $PREFIX/share/fish/vendor_completions.d/rootd.fish

# 4. the autostart block, if you enabled it
#    (delete the lines between the two rootd-fs markers)
nano ~/.bashrc

If an autostart hook is leaving you stuck in a container shell, start a session that skips it first:

ROOTD_NO_AUTO=1 bash
rootd autostart off

That works even when the box itself is broken, because the hook checks the box before entering and falls through to the host shell if it fails.


What this is not

Rootless containers are a convenience boundary, not a security boundary. PRoot and fakechroot emulate privilege; they do not confine a hostile process. Do not use rootd-fs to run code you do not trust.

Port mappings shown by rootd svc are advisory: rootless containers share the host network namespace, so a service binding :8080 is simply reachable at localhost:8080.


Development

python3 -m pytest          # 170 tests, no network needed
python3 -m rootdfs.cli --help

Layer extraction is covered by dedicated traversal and whiteout tests; the shell hook is covered by tests asserting it cannot trap the user.


License

MIT — see LICENSE.

This is original work. It implements published, open specifications (the OCI image spec and the Docker Registry HTTP API v2) and invokes proot and udocker as external programs; it contains no code from those projects.

About

Rootless container filesystems for Termux and Linux — OCI images via PRoot or udocker, no root required

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages