Infrastructure-as-code to deploy a headless Android environment (Waydroid) inside a Debian 13 LXC container on Proxmox, with a browser-based control webapp (screen view + tap/swipe/text, GPS mock-location), device identity spoofing, and GPS injection.
On the Proxmox host, as root:
./0-deploy-all.sh --hostname waydroid --cpu 4 --ram 4096 --disk 16This script:
- Loads the
binder/loopkernel modules on the host. - Creates a privileged Debian 13 container via the community-scripts/ProxmoxVE helper script.
- Injects the binder/apparmor/cgroup configuration into
/etc/pve/lxc/<CTID>.confand restarts the container. - Copies this repo into the container and runs, in order, the Waydroid
install, the systemd services, then the spoofing/GPS tools -
automatically applying a device identity spoof (Pixel 5 by
default; pass
--skip-spoofto leave it unapplied, or--device <profile>/--list-devicesto pick another - see4-waydroid-tools/device-profiles/README.md) and always enabling headless adb authorization (4-waydroid-tools/enable-adb.sh, needed for GPS mock-location and the webapp's screen control to work without a human clicking an "Allow USB debugging" popup), then restarting the container before Waydroid's first boot, so About Phone already shows Pixel 5 the first time you open it. - Enables
waydroid-session.service(auto-start on boot) and, once the session is up, installs and configures the GPS mock-location app automatically (pass--skip-gps-setupto leave it for later), then installs the webapp (5-webapp/- a browser UI plus API for GPS control and adb-based screen control; pass--skip-webappto leave it for later, see5-webapp/README.md).
By default, the webapp is only reachable via an SSH tunnel (see "Security" below). To expose it directly on the LAN (every action still requires its API key):
./0-deploy-all.sh --webapp-expose-lancommunity-scripts/ProxmoxVE is an independent third-party project;
0-deploy-all.sh downloads and runs its ct/debian.sh script on every
run, including when --ctid points at an already-existing container -
only the LXC config injection step (step 3) is actually guarded against
re-running. If you'd rather not run third-party code against an existing
container, or just want to change a setting like --webapp-expose-lan on
an already-deployed one, use the manual per-step scripts below instead of
rerunning 0-deploy-all.sh (see docs/DEBUGGING_AND_TESTS.md, Phase 6, for
the targeted commands).
- Run
1-proxmox-host/enable-binder.shon the Proxmox host. - Create a privileged Debian 13 LXC from the Proxmox UI (or via
community-scripts/ProxmoxVE). - Append the contents of
1-proxmox-host/lxc-config-append.txt(minus comments) to/etc/pve/lxc/<CTID>.conf, then restart the container. - Inside the container:
2-lxc-setup/01-install-waydroid.sh - Then:
3-services/02-install-services.sh - Then:
4-waydroid-tools/03-setup-tools.sh - Optional, to spoof the device (Pixel 5 by default) before first boot
- skip this step to leave it stock, or run
apply-spoof.sh --listto see other options first:4-waydroid-tools/apply-spoof.sh
- skip this step to leave it stock, or run
4-waydroid-tools/enable-adb.sh(setsro.adb.secure=0before first boot - needed soadb/GPS mock-location/the webapp's screen control can authenticate without a human clicking an authorization popup; see "Security" below). Then, since this and/or the spoof step above only take effect on the Android container's next start:systemctl stop waydroid-session && systemctl restart waydroid-containersystemctl start waydroid-session- Optional, to enable GPS mock locations (needs the session running,
unlike the two steps above):
4-waydroid-tools/setup-gps.sh, then4-waydroid-tools/change-location.sh <lat> <lng>. - Optional, for the control webapp (GPS control + adb-based screen
control):
5-webapp/install-webapp.sh. See5-webapp/README.md. - Access: see the message printed at the end of installation, or the "Accessing the interface" section below.
Everything - GPS control and the live screen view/tap/swipe/text control -
is served by the webapp on a single port, gated by a per-deployment API
key (generated on install, printed at the end of install-webapp.sh /
0-deploy-all.sh, and readable afterwards from
/etc/waydroid-webapp/api-token inside the container).
By default (without --webapp-expose-lan / WEBAPP_EXPOSE_LAN=yes), the
webapp only listens on 127.0.0.1 inside the container. From your
machine:
ssh -L 8088:127.0.0.1:8088 root@<LXC_IP>then open http://127.0.0.1:8088/ and enter the API key.
With --webapp-expose-lan / WEBAPP_EXPOSE_LAN=yes, the webapp listens on
all interfaces. Every action still requires the API key - treat it like a
password on an open network. Handy if you already reach your homelab over
a VPN (the VPN then acts as the security perimeter):
./0-deploy-all.sh --webapp-expose-lanthen, from a machine connected to the VPN: http://<LXC_IP>:8088/
WEBAPP_EXPOSE_LAN always defaults to tunnel-only on every run, including
a re-run against an existing deployment - it does not preserve a prior
--webapp-expose-lan. Pass it again explicitly to keep the webapp exposed.
0-deploy-all.sh- Full orchestrator (host -> container -> services).1-proxmox-host/- Scripts/config applied on the Proxmox host.2-lxc-setup/- Installs Waydroid and its dependencies in the container.3-services/- systemd units (Sway headless compositor, Waydroid session) and their runtime helper scripts, installed by02-install-services.sh.4-waydroid-tools/- Device spoofing, GPS, manual startup wrapper, Play Store emulated-storage workaround.5-webapp/- Flask webapp/API for GPS control (map UI, favorites) and adb-based screen control (view + tap/swipe/text) - see5-webapp/README.md.5-webapp/update-webapp.shupdates it in place from GitHub without redeploying the container.tests/-lint.sh(static, no Proxmox needed) andsmoke-test.sh(run inside the container after deployment).docs/- Detailed debugging methodology.
The Wayland compositor used is Sway (wlroots-based), in headless mode
(WLR_BACKENDS=headless, no libinput devices). Waydroid's Android
container needs some compositor present for SurfaceFlinger to render
through (see waydroid-session.service's Requires=), even though
nothing displays its output directly - the webapp captures frames via
adb shell screencap/adbutils instead of reading the compositor's output.
Sway is a well-documented, low-overhead choice for running Waydroid
headless.
- Play Integrity / SafetyNet: some apps (often banking, DRM,
anti-cheat) refuse to start with a message like "For security reasons,
app will close as it does not support devices with modified system".
This is intentional app behavior, not a bug in this deployment:
Waydroid isn't a Google-certified Android device (no locked bootloader,
no hardware attestation).
4-waydroid-tools/apply-spoof.sh(spoofing system properties) may be enough for apps that only check "basic" integrity, but no reliable workaround exists for apps requiring "STRONG" integrity (real hardware attestation) - that's a fundamental limitation, not a configuration issue.
This project makes deliberate, documented security trade-offs, needed to make Waydroid's binder device access work inside an LXC without rebuilding a kernel module:
- **Privileged container +
apparmor unconfined+cgroup2.devices.allow: a- empty
cap.drop** (1-proxmox-host/lxc-config-append.txt): the container gets much broader hardware access and capabilities than a standard LXC, weakening isolation from the Proxmox host. Only deploy this container on a trusted host/LAN.
- empty
- Webapp: every action, including the screen view, is API-key-gated
(
5-webapp/, installed by default): by design, this repo binds it to127.0.0.1and recommends an SSH tunnel.--webapp-expose-lan/WEBAPP_EXPOSE_LAN=yeslifts that restriction, but every webapp action - including fetching a screenshot or sending a tap - requires the per-deployment API key, so exposing it on the LAN means trusting that key like a password, not handing out an open view. See5-webapp/README.md's "Security" section for the full breakdown (favorites stored in plain JSON, Nominatim as a third-party geocoding dependency, etc.). - Device identity spoofing, applied automatically (
4-waydroid-tools/apply-spoof.shdevice-profiles/): appends a fixed block ofro.product.*,ro.build.*, and relatedro.*build properties (system/vendor/odm/bootimage) towaydroid_base.propso About Phone reports a real device instead of "WayDroid x86_64" - by default, Pixel 5 (--skip-spoofto leave it unapplied). The property values are vendored in this repo (4-waydroid-tools/device-profiles/pixel-5.prop), not downloaded at install time - review them directly, and see4-waydroid-tools/device-profiles/README.mdfor where they come from (credit: Quackdoc/waydroid-scripts) and how to add another device with--device <profile>. Apply/re-apply viaapply-spoof.sh, not by hand - it snapshots the pre-spoof configuration, makes re-applying idempotent, and can roll the change back (seedocs/DEBUGGING_AND_TESTS.md, Phase 4).
ro.adb.secure=0, applied automatically (4-waydroid-tools/enable-adb.sh): disables Android's normal adb RSA-key authorization popup, the same way real emulators do by default, soadb/waydroid adb connect(and therefore GPS mock-location and the webapp's screen control) work without a human clicking "Allow USB debugging" at the exact moment Android boots. This means any host that can reach the container'swaydroid0bridge IP can attach a full adb shell with no authentication - in practice that bridge is only reachable from inside the LXC itself, not the wider LAN, so the exposure is no broader thanwaydroid shellalready has. Roll back with4-waydroid-tools/apply-spoof.sh --rollback(shares its backup/restore with the spoof step) followed by a container restart.- Signed third-party app, installed automatically (GPS mock location):
4-waydroid-tools/setup-gps.shinstalls the official, open-source Appium Settings app (io.appium.settings, a signed release from the Appium project - the same mock-location provider used across the Appium/UiAutomator2 mobile-testing ecosystem) as the system's mock-location provider, thenchange-location.shdrives it over a realadbclient connected viawaydroid adb connect(seedocs/DEBUGGING_AND_TESTS.md, Phase 5, for why it isn'twaydroid adb shell/install- those don't exist).0-deploy-all.shruns this automatically once the session is up; pass--skip-gps-setupto leave it for later. This replaces an earlier, non-functional approach that set Waydroid's ownpersist.waydroid.fake_gpsproperty directly - that property isn't consumed by anything in this Android image (seedocs/DEBUGGING_AND_TESTS.md, Phase 5, for how this was confirmed). curl | bashfor the official Waydroid installer (repo.waydro.id, in01-install-waydroid.sh): standard practice in the Waydroid ecosystem, but still a supply-chain risk worth knowing about.
See docs/DEBUGGING_AND_TESTS.md for the full debugging methodology, and
tests/ for automated validation.