Turn Android phones into Kubernetes worker nodes — no root required.
Standard kubectl apply workloads run on mobile SoCs. The control plane (k3s or vanilla k8s) needs no modification. From the scheduler's perspective a DroidNode device is an ordinary Linux worker node.
$ kubectl get nodes
NAME STATUS ROLES AGE
kali Ready master 4d
droidnode-b96a49db497af1eb Ready <none> 2h ← Android phone
$ kubectl apply -f workload.yaml
$ kubectl logs my-pod
hello from DroidNode
architecture: aarch64
- The Android APK starts a foreground service that spawns the Rust agent as a native subprocess.
- The Rust agent registers the device with k3s as a virtual kubelet node and opens an HTTPS endpoint that implements the Kubernetes kubelet API.
- When k3s schedules a pod, the agent pulls the OCI image layers, unpacks them into a rootfs, and runs the container via proot — a ptrace-based userspace chroot that requires no root.
- Pod status and logs flow back to the control plane in real time.
No root. No custom kernel modules. No k3s patches.
DroidNode follows The Standard (Hassan Habib) strictly:
Brokers → Foundation Services → Orchestration Services → Exposers
agent/
├── executor/src/
│ ├── brokers/
│ │ ├── oci_registry_broker.rs # Pull OCI manifests and layers
│ │ ├── filesystem_broker.rs # Layer storage on-device
│ │ ├── proot_broker.rs # Spawn proot containers
│ │ └── control_plane_broker.rs # k8s API client
│ ├── services/foundation/
│ │ ├── image_pull_service.rs # Fetch + cache OCI layers
│ │ ├── image_unpack_service.rs # Unpack tarballs to rootfs
│ │ ├── workload_execution_service.rs
│ │ ├── health_probe_service.rs
│ │ ├── node_capability_service.rs
│ │ └── event_recording_service.rs
│ ├── services/orchestration/
│ │ ├── image_orchestration_service.rs # Pull → unpack → rootfs
│ │ ├── reconciliation_service.rs # Desired vs actual loop (15 s)
│ │ ├── workload_lifecycle_service.rs # Start / stop / status
│ │ └── node_registration_service.rs # Register + heartbeat
│ └── exposers/
│ └── virtual_kubelet_exposer.rs # HTTPS kubelet API (:10250)
└── node-agent/src/main.rs # Entry point + config
android/
└── app/src/main/java/com/droidnode/
├── brokers/ # Android system brokers (process, wake lock, battery, network)
├── services/ # AgentLifecycleService, ResourceGuardService
├── services/orchestration/ # NodeReadinessService
├── exposers/ # ForegroundServiceExposer (Android foreground service)
├── ui/ # DebugActivity — on-device LOGS / STATUS / PODS UI
└── LogBuffer.kt # Ring buffer bridging agent stdout to the debug UI
| Requirement | Version | Notes |
|---|---|---|
| Android device | — | Developer mode + USB debugging enabled (Settings → About phone → tap Build number 7×) |
| Rust + Cargo | stable | rustup target add aarch64-linux-android |
| Android NDK | r27c+ | Set ANDROID_NDK_HOME or let the script download it |
| Android SDK | API 26+ | Android Studio or sdkmanager |
| Java | 17 | For Gradle |
patchelf |
any | sudo apt install patchelf |
adb |
any | For sideloading |
| k3s | v1.28+ | On the control-plane machine |
./scripts/build-android.shThis will:
- Download NDK r27c if
ANDROID_NDK_HOMEis not set - Cross-compile
node-agentforaarch64-linux-android - Download Termux proot + libtalloc, patch RPATH/NEEDED for Android
- Extract the proot ELF loader and rename it
libproot_loader.so - Copy all four
.sofiles intoandroid/app/src/main/jniLibs/arm64-v8a/
cd android
./gradlew assembleDebug
# APK → android/app/build/outputs/apk/debug/app-debug.apkOr open android/ in Android Studio and click Run.
adb install android/app/build/outputs/apk/debug/app-debug.apk# Create the RBAC service account the agent authenticates as
kubectl apply -f deploy/rbac.yaml
# Generate a kubeconfig scoped to the droidnode-agent service account
# (example using k3s — adjust for your cluster)
TOKEN=$(kubectl -n kube-system create token droidnode-agent)
kubectl config set-credentials droidnode-agent --token="$TOKEN"
kubectl config set-context droidnode --cluster=<your-cluster> --user=droidnode-agent
kubectl config use-context droidnode
kubectl config view --minify --flatten > /tmp/droidnode-kubeconfigThe agent generates a kubelet TLS certificate on first start. k3s must trust its CA:
# After first APK launch, copy the CA from the device
adb shell run-as com.droidnode cat /data/data/com.droidnode/files/kubelet-ca.crt \
> /tmp/android-kubelet-ca.crt
# Trust it in k3s
sudo cp /tmp/android-kubelet-ca.crt /etc/rancher/k3s/
# Add to /etc/rancher/k3s/config.yaml:
# kubelet-certificate-authority: /etc/rancher/k3s/android-kubelet-ca.crt
sudo systemctl restart k3sadb push /tmp/droidnode-kubeconfig \
/data/data/com.droidnode/files/kubeconfigLaunch the DroidNode app and tap Start Agent. The node appears in kubectl get nodes within ~30 seconds.
| Variable | Default | Description |
|---|---|---|
KUBECONFIG |
— | Path to kubeconfig (required) |
DROIDNODE_NODE_ID |
droidnode-<hostname> |
Node name as it appears in kubectl |
DROIDNODE_PROOT_PATH |
~/.droidnode/proot |
Path to proot binary |
DROIDNODE_LAYERS_DIR |
~/.droidnode/layers |
OCI layer tarball cache |
DROIDNODE_ROOTFS_DIR |
~/.droidnode/rootfs |
Merged container rootfs trees |
DROIDNODE_KUBELET_PORT |
10250 |
Port for the virtual kubelet HTTPS server |
RUST_LOG |
— | Tracing filter — e.g. info, debug |
Note: k3s runs its own kubelet on port 10250. Use
DROIDNODE_KUBELET_PORT=10255when running alongside k3s on the same machine.
mkdir -p /tmp/droidnode/{layers,rootfs}
KUBECONFIG=~/.kube/config \
DROIDNODE_NODE_ID=droidnode-dev \
DROIDNODE_PROOT_PATH=/usr/bin/proot \
DROIDNODE_LAYERS_DIR=/tmp/droidnode/layers \
DROIDNODE_ROOTFS_DIR=/tmp/droidnode/rootfs \
DROIDNODE_KUBELET_PORT=10255 \
RUST_LOG=info \
cargo run -p node-agent# workload.yaml
apiVersion: v1
kind: Pod
metadata:
name: hello-android
spec:
nodeName: droidnode-<device-id> # from kubectl get nodes
restartPolicy: Never
containers:
- name: hello
image: alpine:latest
command: ["/bin/sh", "-c"]
args: ["echo hello from DroidNode; uname -m"]kubectl apply -f workload.yaml
kubectl logs hello-android
# hello from DroidNode
# aarch64Tap the persistent notification to open the debug UI:
- LOGS — live color-coded stream from the agent
- STATUS — uptime, node ID, running state
- PODS — pod lifecycle events parsed in real time
See open issues for the full list. Major gaps today:
| Area | Status |
|---|---|
| Multi-container pods | Only the first container runs |
restartPolicy: Always |
Not implemented — pods run once |
| Volume mounts | Not functional (source path mapping is wrong) |
| Resource limits | CPU/memory requests and limits are ignored |
| Graceful shutdown | Containers receive SIGKILL, no SIGTERM grace period |
| Load balancer pods (svclb) | iptables calls fail inside proot |
| Network isolation | Containers share the host network namespace |
kubectl exec / kubectl port-forward |
Not implemented |
| ARM32 / x86 devices | Only aarch64 tested |
- Fork and clone the repo.
- Run
./scripts/build-android.shto get the binaries. - Open
android/in Android Studio for Kotlin work; the Rust crates are inagent/. - Run Rust checks locally before pushing:
cd agent && cargo check --workspace && cargo clippy --workspace
- Open a PR — CI runs
cargo check,cargo clippy, andcargo testautomatically.
Architecture rule: no layer skips. Brokers call external systems only. Foundation services do one thing. Orchestration services coordinate. Exposers are thin entry points. PRs that skip layers will be asked to refactor.
MIT