DroidFleet documentation

DroidFleet lets you install APKs on a fleet of real Android phones over the internet, stream their logs back in real time, collect crash reports, and diff UI screenshots between builds. No cables. No cloud test farm. Your phones, your data.

Install

Two paths: self-host (for teams) or use the desktop app (for a single developer).

Desktop

1. Download DroidFleet.exe from the releases page.
2. Double-click — it opens the UI in a window.
3. Click + → scan QR with each phone you want to pair.

Self-host

curl -sSL https://get.droidfleet.app/install.sh | bash -s -- \
  --domain droidfleet.example.com \
  --email [email protected]

Deploys a Docker-based install in ~2 minutes. Needs a VPS with a public IP + DNS record.

Pair a phone

  1. In the desktop app, click the + button in the sidebar.
  2. Scan the QR code with the phone's camera. This opens a URL that downloads the DroidFleet agent APK.
  3. On the phone: tap Install, then open DroidFleet, then flip the Switch to Active.
  4. Within ~10 seconds the phone appears in the desktop app's sidebar with a green dot.

Authentication

Two mechanisms — pick the right one for your use case:

MechanismWhen to useHow
JWTInteractive (web UI, desktop)POST /auth/login → Bearer token
API keyCI/CD, scriptsX-API-Key: df_live_...

Phones

# list
curl -H "X-API-Key: df_live_xxx" https://your.domain/api/phones

# rename
curl -X POST -H "X-API-Key: ..." -d '{"friendlyName":"Test phone 1"}' \
  https://your.domain/api/phones/p_xxx/rename

# ping (vibrate + toast — for physical identification)
curl -X POST -H "X-API-Key: ..." https://your.domain/api/phones/p_xxx/ping

Installing APKs

curl -X POST -H "X-API-Key: ..." \
  -F "apk=@./build/app.apk" \
  "https://your.domain/install-wireless?phoneId=p_xxx&gitSha=abc1234"

The gitSha + gitBranch query params are optional; when present they're stamped on the install session for later correlation with crash reports.

Live logs (SSE)

curl -N "https://your.domain/stream-logs?phoneId=p_xxx&package=com.my.app"

Server-Sent Events — each line is data: {"type":"log","line":"..."}\n\n. Use from any language that speaks HTTP+SSE.

Crashes

The agent's UncaughtExceptionHandler reports crashes automatically over the relay. If you want to report from your own app:

curl -X POST -H "X-API-Key: ..." -H "Content-Type: application/json" \
  -d '{"stackTrace":"java.lang.NullPointerException\n   at...", "packageName":"com.my.app"}' \
  https://your.domain/api/crashes

Dedup: the server hashes the top 5 app frames. Identical crashes merge into a single CrashGroup.

A/B cohorts

curl -X POST -H "X-API-Key: ..." -H "Content-Type: application/json" \
  -d '{"name":"canary-20pct","percent":20,"gitSha":"abc123"}' \
  https://your.domain/api/cohorts

Phones are assigned to cohorts deterministically via hash(phoneId + cohortName) % 100. Stable across restarts.

Webhooks

Register a URL that receives POSTs for events you care about:

Payloads are signed with HMAC-SHA256 via the X-DroidFleet-Signature header. Verify before trusting.

Persona — local emulator farm

Persona is the emulator side of DroidFleet. It boots isolated Android emulators on your machine, each with its own apps, storage, accounts, and an optional WireGuard tunnel for distinct exit IPs. Every emulator that boots automatically registers with your DroidFleet server as a phone — they appear in /api/phones next to your real devices. Same UI, same install pipeline, same logs.

# From the repo root
pnpm persona doctor                      # check prerequisites
pnpm persona provision                   # create AVDs from config/users.yaml
pnpm persona:up                          # boot all users in parallel
pnpm persona:scenarios                   # run smoke YAML scenario

Lives under apps/persona/ in the repo. The bridge auto-installs the DroidFleet agent APK on each emulator and points it at $DROIDFLEET_SERVER_URL (default http://localhost:3100). Set DROIDFLEET_DISABLE=1 to opt out, or PERSONA_ENABLED=1 on the server to expose the admin endpoints.

Persona API

All endpoints below require an admin session. Two internal endpoints (/enroll-hint, /crash-ingest) accept a shared-secret token (DROIDFLEET_INGEST_TOKEN) or loopback-only.

GET  /api/persona/users              # list users from config/users.yaml
GET  /api/persona/scenarios          # list YAML scenarios
GET  /api/persona/status             # current state of all users
POST /api/persona/up                 # { users: [...] } — boot emulators
POST /api/persona/down               # { users: [...] } — stop emulators
POST /api/persona/scenarios/run      # { scenario, users? } — SSE stdout/stderr stream
POST /api/persona/enroll-hint        # internal — friendly-name hint
POST /api/persona/crash-ingest       # internal — multipart crash forward

Scenarios

Declarative YAML tests under apps/persona/scenarios/. The runner boots target users in parallel, drives the UI via uiautomator2, and captures artifacts on failure into a single HTML report.

name: login-flow
users: all
tests:
  - name: login_succeeds
    steps:
      - action: launch_app
        package: com.example.app
      - action: type_text
        res_id: com.example.app:id/username
        text: [email protected]
      - action: tap
        res_id: com.example.app:id/submit
      - action: wait_for
        text: "Welcome, alice"
      - action: screenshot
        tag: logged-in

21 built-in actions. Run from the UI at /ui/persona.html with live SSE streaming, or via CLI: pnpm persona test run login --open.

Self-hosting

Docker

git clone https://github.com/droidfleet/droidfleet
cd droidfleet
docker compose up -d

Exposes port 3100. For TLS, put Caddy or Nginx in front — see scripts/nginx.conf.example.

Switching to Postgres

DATABASE_URL_PG=postgresql://u:p@host:5432/droidfleet \
  ./scripts/migrate-to-pg.sh

When to migrate: >1GB data, >5 concurrent users, need read replicas. Below that, SQLite is fine.

Your own relay

The default relay (appproof-relay.onrender.com) is shared infrastructure — fine for evaluation, bad for production. Deploy your own:

cd apps/relay
docker build -t your-org/relay .
# Then deploy to Fly.io, Railway, or any container host.
# Set RELAY_WS_BASE=wss://your-relay.example.com in the server's .env.

/version endpoint

Returns:

{
  "server":            "droidfleet",
  "serverVersion":     "1.0.0",
  "minAgentVersion":   "1.0.0",
  "apkUrl":            "https://tunnel.trycloudflare.com/ui/testflow.apk",
  "apkSha256":         "...",
  "apkBuildTime":      "2026-04-20T14:11:34.573Z",
  "apkSizeBytes":      12238177
}

Agents check this every few heartbeats and auto-update when they're below minAgentVersion.

/metrics

Prometheus exposition format. Scrape with:

scrape_configs:
  - job_name: droidfleet
    static_configs:
      - targets: ['droidfleet.example.com']
    metrics_path: /metrics
    scheme: https

Exposed series:

Security model

Full threat model: SECURITY.md. Report vulnerabilities to [email protected].

Troubleshooting

Phone doesn't appear after pairing

"This phone can't install the APK"

Logs freeze every few seconds

© 2026 DroidFleet · App · Status · Support