Multiple Instances¶
One of Beetroot's core features is running several independent Android research phones side by side, each with its own /data, ports, resource caps, and config. This page covers how to set them up and coordinate across them.
Create multiple instances¶
beetroot create research-clean
beetroot create research-stealth
cp examples/stealth.yaml research-stealth/beetroot.yaml
beetroot apply research-stealth
beetroot up research-clean research-stealth
up accepts multiple names in one call. Each instance starts in parallel.
Port allocation¶
Ports are assigned by index — the lowest free non-negative integer at creation time. The stride is 10:
| Index | ADB port | Frida port | Frida control |
|---|---|---|---|
| 0 | 5555 | 27042 | 27043 |
| 1 | 5565 | 27052 | 27053 |
| 2 | 5575 | 27062 | 27063 |
| N | 5555+N×10 | 27042+N×10 | 27043+N×10 |
Index is assigned at create time and freed on destroy. If you destroy index 1 and create a new instance, it gets index 1 again. The index (and therefore ports) never change for the lifetime of an instance.
beetroot ls always shows the current mapping:
NAME KIND IDX ADB FRIDA STATUS PATH
research-clean redroid 0 localhost:5555 localhost:27042 running /home/you/research-clean
research-stealth redroid 1 localhost:5565 localhost:27052 running /home/you/research-stealth
Pinning ports¶
By default ports are allocated from the stride table above. Since api_version: 8 the ports: block is a list of {service, guest, host} mappings (issue #108). To pin a port for a specific instance (for example, to keep a stable host port across destroy/recreate cycles, or to coordinate with a tool that targets a fixed port), give the entry an explicit host:
ports:
- {service: adb, guest: 5555, host: 9000} # pin ADB to 9000
- {service: frida, guest: 27042} # host unset → stride default
- {service: frida_control, guest: 27043}
- {guest: 8080, host: 9090} # forward an arbitrary in-guest port
Then re-stage:
An entry whose host is unset falls back to the stride allocation, so you can pin one port (say, ADB) and leave the Frida ports on stride. You can also forward arbitrary in-guest ports beyond the three well-known services.
If you pin a port that another instance already uses, beetroot create and beetroot apply exit with a clear error before staging:
See ports in the config reference for the full schema.
Working with a specific instance¶
Every Beetroot verb that targets an instance takes its name:
beetroot shell research-clean # adb shell into instance 0
beetroot logs research-stealth -f # tail logs of instance 1
beetroot down research-clean # stop instance 0 only
down and up also accept multiple names:
Scripting across instances¶
Use beetroot status to get machine-readable addresses for one instance:
eval $(beetroot status research-clean | python3 -c "
import json,sys
r=json.load(sys.stdin)
print(f'ANDROID_DEVICE={r[\"adb_address\"]}')
print(f'FRIDA_DEVICE={r[\"frida_address\"]}')
")
# $ANDROID_DEVICE = localhost:5555
# $FRIDA_DEVICE = localhost:27042
adb -s "$ANDROID_DEVICE" install ./target.apk
frida -H "$FRIDA_DEVICE" -n com.target.app
For automation that touches all running instances, iterate over beetroot ls --json:
beetroot ls --json | python3 -c "
import json, sys
for name, meta in json.load(sys.stdin).items():
if meta['status'] == 'running':
print(name, meta['adb'])
"
Resource budgeting¶
Each instance at default settings uses roughly 1.2 GB RAM / <5% CPU at idle, rising to ~2.5 GB when a Frida-instrumented app is active. At the defaults (mem: 3g, cpus: 2.0):
- 3 instances = ~9 GB committed / 6 vCPUs
- 5 instances = ~15 GB committed / 10 vCPUs
Override per-instance by editing the resources: block in that instance's beetroot.yaml and running beetroot apply <name>. You generally want to lower the FPS and resolution before raising RAM/CPU — display overhead is non-trivial.
Stopping and cleaning up¶
# Stop all running instances without deleting data
beetroot down research-clean research-stealth
# Destroy (wipe data) a specific instance
beetroot destroy research-stealth
Destroy is permanent
beetroot destroy deletes the entire instance directory, including /data. Use beetroot down if you want to keep state.