Docker container that runs Android cuttlefish emulators for x86_64, arm64, riscv64 guests
I started with a simple desire to run RISC-V AOSP cuttlefish image on my x86_64 laptop, and the journey wasn't smooth.
- The info is scarce, broken and conflicting
- My host machine ended up seriously "polluted" and cluttered by packages, virtual devices and run-time artifacts
- The existing container-based solution by Google is an overkill and not friendly at all
- Fundamentally I realized that Android Cuttlefish was meant for cloud and infra usage, not for an individual on a laptop.
Thus this project was born. And it expanded.
- Simple container that runs 1 cuttlefish instance at a time
- Docker bridge networking keeps the host clean; published ports provide reachability and convenience
- Support all 3 arches of hosts, x86_64, arm64 and riscv64
- Support all 3 arches of guests, x86_64, arm64, and riscv64
- Support webrtc UI connection for crosvm and VNC UI connection for qemu
- Support adb connection
See host/guest support matrix below. Same-arch host/guest defaults to crosvm (qemu selectable via flag); cross-arch is qemu only.
| Host\Guest | x86_64 | arm64 | riscv64 |
|---|---|---|---|
| x86_64 | crosvm ✓ qemu ✓ |
qemu ✓ | qemu ✓ |
| arm64 | qemu ❓ | crosvm ⚠ qemu ❓ |
qemu ❓ |
| riscv64 | qemu ✗ | qemu ✗ | crosvm ✓ qemu ✗ |
Legend: ✓ verified · ⚠ supported but unverified · ❓ unknown · ✗ unsupported
4 steps to use the container:
- build the docker image with "cf-build.sh" (once)
- create or update the cvd instance with "cf-init.sh ..." (infrequently, as needed)
- populate/update instance with aosp img zip and/or cvd host package
- run the cvd instance with "cf-run.sh" (frequently)
- connect to the instance via adb, vnc or webrtc
- stop cvd instance with "cf-stop.sh"
Below is a sample execution sequence on x86_64 host.
./cf-build.sh
./cf-init.sh -P aosp_cf_x86_64_only_phone-img-14421689.zip -H cvd-host_package.tar.gz
# -H and -P also take URLs
./cf-init.sh -P https://example.com/aosp_cf_riscv64_phone-img.zip \
-H https://example.com/cvd-host_package_riscv64.tar.gz
# run with default crosvm
./cf-run.sh
firefox https://localhost:8443
# or run with qemu
./cf-run.sh --vm-manager qemu_cli
gvncviewer localhost
# connect via adb
adb connect localhost:6520
For x86_64 and arm64, you can find the AOSP image zip and cvd host package from Google aosp build artifacts site. Or build them from your own aosp tree,
source build/envsetup.sh
lunch aosp_cf_x86_64_only_phone-aosp_current-userdebug
m dist
# those two packages can be found under $(ANDROID_ROOT)/out/dist/
For RISCV64, refer to the central github project, aosp-cuttlefish-riscv64
Additional notes:
- run "cf-build.sh -h", "cf-init.sh -h", "cf-run.sh -h", "cf-stop.sh -h" for more info
- to create multiple containers/instances, create multiple directories. For example, "mkdir riscv; cd riscv; ../cf-init.sh ....; ../cf-run.sh".
- However, you can only run 1 of them at any time due to port conflict.
- ADB, VNC ports are visible to host LAN. So you can run it on a headless server.
entrypoint.sh populates /cf itself when it is empty and it knows where to
fetch from, so an instance can start without running cf-init.sh:
docker run -it --rm --privileged --ulimit nofile=65536:65536 \
-p 8443:8443 -p 5900:5900 -p 6520:6520 \
-p 15550-15599:15550-15599/udp \
-v "$PWD/cf-data:/cf" \
-e CF_HOST_PACKAGE_URL=https://example.com/cvd-host_package_riscv64.tar.gz \
-e CF_PRODUCT_IMG_URL=https://example.com/aosp_cf_riscv64_phone-img.zip \
cf-host
Host and product are fetched independently, so a partly populated /cf is
completed rather than rebuilt. Neither variable set and /cf empty is an
error, as before.
To ship an image that carries its own sources and needs no -e, add the two
variables in a downstream image:
FROM cf-host
ENV CF_HOST_PACKAGE_URL=... \
CF_PRODUCT_IMG_URL=...
A browser on the Docker host works as-is. A browser elsewhere needs
-e CF_EXTRA_HOST_IPS=<host-lan-ip> — the container sees only its own veth
address and cannot discover the host's.
- The generic image ships no GPU stack; gpu_mode resolves to guest_swiftshader. GPU support is host-specific -- see Dockerfile.bianbu-k3.
Plain and standard docker bridge mode, no additional tun/tap/etc network interface pollution to the host. Container reaches outbound via docker0 MASQUERADE; clients reach inbound via published host ports:
8443/tcp— WebRTC HTTPS (crosvm path)5900/tcp— VNC (qemu path; in-container socat bridge from launch_cvd's127.0.0.1:6444)6520/tcp— adb15550-15599/udp— WebRTC ICE media (crosvm path)
VNC and adb bind 0.0.0.0; reachable from anywhere on the host LAN. VNC needs no further wiring — gvncviewer <host> works.
WebRTC needs more care:
- cvd is pinned to UDP
15550-15599via--udp_port_range(cf dialect) /--webrtc_udp_port_range(AOSP), forwarded 1:1 to the host. Static DNAT, no STUN/TURN dependency — the browser sends UDP straight to<host-ip>:15550-15599and docker DNATs it into the container. cf-run.shauto-detects the host's outward IPv4 addresses (LAN, tailscale, VPN) and injects them as ICE host candidates, so the browser actually has a reachable address to try.- Override or disable with
--docker-arg "--env=CF_EXTRA_HOST_IPS=<csv>"(empty = off; reasonable when both peers have public IPs and srflx hole-punching already works).
