<div align="center">
<h1>dorker</h1>
<p><strong>Docker for headless Android emulator environments</strong></p>
<p>
<img alt="License" src="https://img.shields.io/badge/license-MIT-green">
<img alt="Rust" src="https://img.shields.io/badge/rust-1.85%2B-orange">
<img alt="Edition" src="https://img.shields.io/badge/edition-2024-blue">
<a href="https://crates.io/crates/dorker"><img alt="crates.io" src="https://img.shields.io/crates/v/dorker.svg"></a>
</p>
<p>
<a href="#install">Install</a>
·
<a href="#quickstart">Quickstart</a>
·
<a href="#development">Development</a>
</p>
</div>
---
## Install
Requires [Rust](https://rustup.rs) **1.85+** and `~/.cargo/bin` on your `PATH`.
```bash
cargo install dorker
```
Verify:
```bash
dorker --help
```
<details>
<summary><strong>Build from source</strong> — for development or unreleased changes</summary>
```bash
git clone https://github.com/cesarferreira/dorker.git
cd dorker
cargo install --path . --locked
# or
make install-release
```
Debug install (faster compile, larger binary):
```bash
make install
```
Run without installing:
```bash
make build-release
./target/release/dorker
```
</details>
<a id="quickstart"></a>
## Quickstart
Requires the Android SDK (`emulator`, `platform-tools`) and an installed system
image (`dorker pull` installs one). macOS Apple Silicon and Linux x86_64 with KVM are
validated. No Docker needed.
```bash
# Check your setup, then install a system image if one is missing
dorker doctor
dorker pull android:35
# Boot a headless emulator and use it
dorker run -d --name test android:35
dorker ps
dorker exec test "getprop ro.build.version.sdk"
dorker install test ./app-debug.apk
dorker screenshot test shot.png # waits up to 10s for a non-blank frame
dorker logs test # add -f to follow
# Stop it, bring it back (cold boot, keeps its data), then delete it
dorker stop test
dorker start test
dorker rm test
```
### Describe your environment in a Dorkerfile
A `Dorkerfile` (next to your app, like a Dockerfile) declares the Android environment: the
base image, resources, the APK to install, settings, and a snapshot to start from.
```dorkerfile
# Dorkerfile
FROM android:35
MEMORY 2GB
CPUS 2
DISK 4GB
DEVICE pixel_7
RESOLUTION 1080x2400
HEADLESS true
# Paths are relative to this file's folder.
INSTALL ./app/build/outputs/apk/debug/app-debug.apk
# Turn off animations: faster, less flaky UI tests.
SETTING window_animation_scale 0
SETTING transition_animation_scale 0
SETTING animator_duration_scale 0
# Save a snapshot so instances start in a few seconds,
# and open the app once an instance is ready.
SNAPSHOT ready
CMD launch com.example.app
```
Then build the image once and run it as many times as you like:
```bash
# Install the base image named by FROM, if it is missing
dorker pull
# Build once: installs your APK, applies settings, saves the snapshot
dorker build -t myapp:latest .
# Run as often as you like; each run starts from the snapshot
dorker run -d --name t1 myapp:latest
dorker screenshot t1 shot.png
dorker test t1 \
--runner com.example.app.test/androidx.test.runner.AndroidJUnitRunner
dorker rm -f t1
# Or run in the foreground; the instance is removed on Ctrl-C
dorker run --rm myapp:latest
```
An identical rebuild is a cache hit (instant), and every `dorker run` gets its own isolated
copy, so instances never share state. To try it without your own app, use the sample:
```bash
# Build the tiny sample APK (needs the Android build-tools and a JDK)
examples/basic/make-apk.sh
dorker build -t basic examples/basic
dorker run -d --name b1 basic
dorker exec b1 "pm list packages | grep dorker"
dorker rm -f b1
```
Every directive is documented in [docs/dorkerfile.md](docs/dorkerfile.md). More examples:
[examples/basic](examples/basic), [examples/instrumentation](examples/instrumentation) and
[examples/counter-app](examples/counter-app), a real Gradle app with Espresso tests
(`./dev.sh` runs Gradle, `dorker build`, `run`, a screenshot and the tests). The example APKs
are not committed: build them with each example's `make-apk.sh` (or Gradle for the counter app).
Each instance gets its own private AVD under `~/.dorker` (override with
`DORKER_HOME`). Your existing AVDs are never touched. `dorker run --rm` removes the
instance when it stops. `ps`, `images` and `doctor` accept `--json`.
Measured on an Apple M4 Pro, API 35, 2GB: cold boot about 15s, snapshot restore
about 4s. Host memory is about 2.5GB after a cold boot and 3.3GB after a restore.
Details and caveats: [docs/benchmarks.md](docs/benchmarks.md).
### Test, benchmark, automate
```bash
dorker init # write a starter Dorkerfile
dorker test t1 \
--runner com.example.test/androidx.test.runner.AndroidJUnitRunner
dorker test t1 --runner ... --class com.example.FooTest#bar
# Boot, restore, launch, disk and idle numbers
dorker bench myapp:latest --runs 3 --json
# Also time the plain official emulator, for comparison
dorker bench android:35 --baseline
```
`dorker test` prints a summary, preserves the raw instrumentation output and logcat
under the instance directory, and exits 7 if anything failed.
See [examples/instrumentation](examples/instrumentation) for a runnable example.
### Tests
```bash
# Unit, CLI and fake-SDK lifecycle/failure tests (no emulator needed)
cargo test
# End-to-end: boots real emulators (about 3 minutes)
cargo test -- --ignored --test-threads=1
```
### Use it from AI agents
`dorker mcp` is a Model Context Protocol server: an agent can start instances, install apps,
run commands, take screenshots, read logs and run tests, all with structured results.
```bash
claude mcp add dorker -- dorker mcp
```
See [docs/mcp.md](docs/mcp.md) for the tools, other clients and security notes. The same
operations are available to scripts as `--json` on `run`, `start`, `exec`, `install`,
`screenshot`, `test`, `ps`, `images`, `doctor` and `bench`.
### Diagnostics
`dorker doctor` colours its report on a terminal (green ok, yellow warn, bold red FAIL, fixes
in cyan). Colour is off when piped, `NO_COLOR` turns it off, `CLICOLOR_FORCE=1` forces it on,
and `--json` is never coloured.
`dorker -v <command>` (or `DORKER_VERBOSE=1`) prints what dorker decides and runs to stderr: the
located SDK, the boot mode and why, the exact emulator command line and environment, state
transitions and timings. Each instance also keeps its raw emulator output in
`~/.dorker/instances/<id>/emulator.log`.
### Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | other error |
| 2 | invalid configuration or arguments (including Dorkerfile errors) |
| 3 | missing dependency (SDK, emulator, adb, system image); `doctor` failures |
| 4 | image build failed |
| 5 | instance failed to start or boot |
| 6 | adb operation failed |
| 7 | instrumentation tests failed |
| 8 | name, port or build conflict, or not enough free disk space |
`dorker exec` returns the guest command's own exit status.
See [docs/architecture.md](docs/architecture.md) for design and limitations.
## Limitations
- **Platform:** validated on macOS Apple Silicon (primary) and Linux x86_64 with KVM (Ubuntu 24.04, an Azure VM with nested virtualisation; unit tests and the full emulator end-to-end suite pass). Linux arm64 and bare-metal Linux are untested; Windows is unsupported. Linux is slower in our measurements (cold boot about 29s, snapshot restore about 9s; see [docs/benchmarks.md](docs/benchmarks.md)).
- **System images are installed on request, not automatically.** `dorker pull android:35` runs Google's `android sdk install` (the replacement for the deprecated `sdkmanager`; older command-line tools fall back to `sdkmanager`). The new CLI accepts the Android SDK licences (and the ARM translation licence for arm64 images) for you without prompting, so `pull` says so first, and Google documents that it collects usage data (opt out with `android --no-metrics`); `dorker run`/`build` print this command when an image is missing. The download path itself was only verified for the already-installed and nonexistent-package cases, not a real multi-GB download.
- **Snapshots are strict.** A change of emulator version, system-image revision, memory or CPUs invalidates an image's snapshot. dorker then cold boots (about 15s) and says so. Snapshots are host-specific and not portable between machines.
- **Memory:** a 2GB guest used about 2.5GB of host memory after a cold boot and 3.3GB after a snapshot restore on macOS (3-3.7GB on Linux). Idle CPU is dominated by Android background work and takes about a minute to settle.
- **Builds take about 45s** because dorker reboots the guest to persist provisioned state.
- **`CMD`** only supports `launch <package>`. `dorker start` always cold boots (about 14s) because restoring the image snapshot would roll back the instance's data. There is software rendering (`swiftshader_indirect`) is the default; `GPU <mode>` in the Dorkerfile selects another, but none was faster for snapshot restore in our measurements (see docs/benchmarks.md) and no automatic test-APK discovery.
- **Concurrency:** creating instances concurrently is safe, and `stop`, `rm` and `start` on the same instance are serialised by a per-instance lock (the second command waits up to 60s, then exits 8).
- **Orphans:** emulator processes that dorker started but lost track of (CLI killed mid-start) are reported by `dorker ps`, not adopted or killed automatically.
- `examples/basic` and `examples/instrumentation` use tiny hand-built APKs; `examples/counter-app` is the realistic one.
<a id="development"></a>
## Development
Common tasks via the `Makefile`:
```bash
make # check + build + test
make build # debug build
make build-release
make install # install debug binary
make install-release
make run ARGS="doctor"
make check # cargo check + clippy
make fmt # format
make lint # fmt check + clippy
make test
make clean
make demo # install + show --help
```
Releasing (requires [cargo-release](https://github.com/crate-ci/cargo-release) and [git-cliff](https://github.com/orhun/git-cliff)):
```bash
make release # default minor bump
make release LEVEL=patch # patch bump
make release LEVEL=major # major bump
```
The pre-release hook regenerates `CHANGELOG.md` with `git-cliff` from your conventional-commit history (grouped into Features, Bug Fixes, etc. per `cliff.toml`) and commits it alongside the version bump. Pushing the resulting `v*` tag triggers the release workflow, which builds the multi-platform binaries and publishes a GitHub Release whose notes are generated by `git-cliff` from the same config.
## License
MIT