Filesystem Layout¶
An instance is a directory anywhere¶
Beetroot does not require any central project tree. An instance is just a directory on disk containing a beetroot.yaml file — anywhere you like. The CLI discovers the current instance by walking up from cwd looking for the marker, the way git discovers a repo via .git.
~/research/alpha-phone/ # An instance lives here
├── beetroot.yaml # Source of truth — NOT gitignored
├── .env # Generated from beetroot.yaml
├── data/ # Bind-mount → /data inside Android
├── modules/ # Bind-mount → /data/adb/modules_update (read-only)
└── frida-server # Bind-mount → /data/local/tmp/frida-server
~/elsewhere/bravo-phone/ # A second instance lives wherever
├── beetroot.yaml
├── .env
├── data/
├── modules/
└── frida-server
beetroot create alpha creates the instance directory at ./alpha/ by default. Pass --path /some/where/else to drop it anywhere. Existing directories are adopted via beetroot register <path>.
beetroot.yaml¶
The configuration file for this instance. Not gitignored. If you commit it, you get a reproducible device config — but only the YAML; runtime state never lives in git.
.env¶
Generated by the CLI via config.render_env(). Never hand-edited — edit beetroot.yaml instead and run beetroot apply. Contains all the ${VAR} substitutions that the bundled compose template needs.
data/¶
Redroid backend only. The Android /data partition, bind-mounted read-write. Gigabytes of state; pack into a portable .tar.zst with beetroot snapshot, or use cp -a for a quick same-host rollback.
For a binder: vm instance the guest's /data lives inside the guest rootfs (/var/lib/redroid-data, override with BEETROOT_GUEST_DATA_DIR), not in this host-side directory — so data/ here is vestigial and holds nothing meaningful. An adb-adopted device likewise has no host-side /data. That is why beetroot snapshot/restore are redroid-only: pointing them at a vm or adb instance raises a clear "only supported for the redroid backend … (see issue #128)" error rather than packing an empty data/.
modules/¶
Magisk module staging, bind-mounted read-only at /data/adb/modules_update (v0.4 T4 moved this from the Beetroot-invented /flash_dir; BEETROOT_MODULES_DIR overrides the container-side target). The CLI mirrors beetroot.yaml's modules: list here on create and apply.
frida-server¶
The Frida server binary for this instance, bind-mounted at /data/local/tmp/frida-server. When beetroot.yaml declares a frida: block, this file is downloaded from GitHub releases and cached host-wide so multiple instances share a single download. When the block is omitted (the v0.3+ default), this file is a 0-byte non-executable placeholder and entrypoint.sh skips the Frida launch. See Frida.
What's at the user level¶
~/.config/beetroot/
└── instances.json # Cross-instance registry: name → absolute_path, index
~/.cache/beetroot/
├── frida/ # Decompressed frida-server binaries by version
└── modules/ # Downloaded Magisk module zips
The registry is a single user-global JSON file. Every instance on the host is listed here regardless of where its directory lives on disk; this is how beetroot ls, beetroot up <name>, etc. find each instance.
Both paths respect the XDG basedir spec:
$XDG_CONFIG_HOME/beetroot/instances.json(default~/.config/beetroot/instances.json)$XDG_CACHE_HOME/beetroot/<subsystem>/(default~/.cache/beetroot/<subsystem>/)
What's inside the wheel¶
The compose template and the Dockerfile contract ship inside the beetroot package itself:
src/beetroot/
├── cli.py
├── compose.py
├── config.py
├── ...
└── templates/
└── compose.yaml # The compose template — same path under uv tool install
There is no compose.yaml at any project root anymore — the CLI resolves the bundled copy via importlib.resources and passes it to compose with -f <bundled-path> plus --project-directory <instance-dir> so the per-instance bind mounts resolve correctly.
Starter beetroot.yaml files (default.yaml, stealth.yaml, no-gapps.yaml, with-frida.yaml) live in the repo's top-level examples/ directory and are not bundled inside the wheel. They are documentation-only — beetroot create doesn't load them. See the examples guide for how to use them.
What to commit¶
Commit beetroot.yaml from any instance dir you want reproducible. Ignore everything else under it (data/, modules/, frida-server, .env).
A collaborator can clone your repo, cd into the instance dir, then:
...and get the same Android version, Frida version, modules, and Magisk config — just with a fresh /data.