Deploy Android Redroid with droidVNC-NG and noVNC
Run Android in Docker with Redroid, control it using droidVNC-NG, expose noVNC through Traefik and Authentik, and keep raw VNC tailnet-only.
Redroid runs Android in a Linux container. It is useful when you want a disposable Android environment on a server, but browser control can be tricky. The stable pattern documented here is:
- Redroid provides the Android runtime.
- droidVNC-NG runs inside Android and handles screen capture plus input.
- noVNC exposes the VNC session in a browser.
- Traefik routes the web UI through HTTPS and Authentik.
- Raw VNC is bound only to a private tailnet IP.
This service has since been retired from the live homelab, but the final working design is documented because it is reusable.
All domains, passwords, and tailnet IPs below are placeholders. Replace android.example.com, 100.64.0.2, and the VNC password with your own values.
Do not publish a raw VNC port on
0.0.0.0. Use a VNC password and bind native VNC to localhost or a private tailnet address only.
What this service does
Route or access pattern:
1
2
3
https://android.example.com -> Authentik -> noVNC browser UI
100.64.0.2:5901 -> optional native VNC on private tailnet only
127.0.0.1:5555 -> local ADB only
Main components:
1
Redroid Android container, droidVNC-NG APK, noVNC, websockify, Docker Compose, Traefik, Authentik, Headscale/Tailscale.
Runtime flow:
1
2
3
4
5
6
Browser
-> Traefik HTTPS
-> Authentik
-> noVNC/websockify on port 6080
-> droidVNC-NG inside Android on port 5901
-> Redroid screen and input
Why VNC instead of Webscreen or scrcpy-over-WebRTC
Redroid on ARM64 can expose edge cases in MediaCodec/scrcpy streaming paths. In this deployment, Webscreen and scrcpy-style WebRTC pages could load but produced black video or unstable frame negotiation. Screenshot polling worked, but it was too slow for real use.
VNC was more practical:
- droidVNC-NG captures the Android screen inside the Android environment.
- noVNC gives browser access without requiring a native VNC app.
- Native VNC remains available on the tailnet for faster clients.
- The browser route stays behind SSO.
Related documentation
- Deploy Headscale and Headscale UI with Docker and Traefik
- Deploy Traefik Reverse Proxy with Docker and Cloudflare
- Deploy Authentik SSO with Docker and Traefik
- Deploy Homepage Dashboard with Docker, Traefik and Authentik
Folder layout
Use one service directory.
1
2
3
4
5
6
7
8
9
/home/ubuntu/android-redroid/
├── docker-compose.yml
├── novnc/
│ └── Dockerfile
├── vnc/
│ └── droidvnc-ng.apk
├── secrets/
│ └── droidvnc.env
└── data/ # Redroid persistent Android data
Create it:
1
2
mkdir -p /home/ubuntu/android-redroid/{novnc,vnc,secrets,data}
cd /home/ubuntu/android-redroid
Download droidVNC-NG
Edit files under /home/ubuntu/android-redroid.
Download the droidVNC-NG APK into /home/ubuntu/android-redroid/vnc/droidvnc-ng.apk.
1
2
cd /home/ubuntu/android-redroid
curl -L -o vnc/droidvnc-ng.apk https://github.com/bk138/droidVNC-NG/releases/download/v2.19.0/droidVNC-NG-2.19.0.apk
If that release URL changes, download the latest APK from the droidVNC-NG GitHub releases page and keep the filename consistent.
VNC secret file
Edit /home/ubuntu/android-redroid/secrets/droidvnc.env.
DROIDVNC_PASSWORD=<strong-vnc-password>
Restrict permissions:
1
chmod 600 /home/ubuntu/android-redroid/secrets/droidvnc.env
noVNC image
Edit /home/ubuntu/android-redroid/novnc/Dockerfile.
1
2
3
4
5
6
7
8
9
FROM python:3.12-alpine
RUN apk add --no-cache curl tar bash && pip install --no-cache-dir websockify==0.12.0 && mkdir -p /opt/novnc && curl -L --fail https://github.com/novnc/noVNC/archive/refs/tags/v1.5.0.tar.gz | tar xz --strip-components=1 -C /opt/novnc && printf '%s
' '<!doctype html>' '<meta charset="utf-8">' '<title>Android VNC</title>' '<meta http-equiv="refresh" content="0; url=/vnc.html?autoconnect=1&resize=scale&reconnect=1">' '<script>location.replace("/vnc.html?autoconnect=1&resize=scale&reconnect=1")</script>' '<a href="/vnc.html?autoconnect=1&resize=scale&reconnect=1">Open Android VNC</a>' > /opt/novnc/index.html
EXPOSE 6080
ENTRYPOINT ["websockify"]
CMD ["--web", "/opt/novnc", "6080", "redroid:5901"]
This makes the root of the web service open noVNC directly.
Docker Compose stack
Edit /home/ubuntu/android-redroid/docker-compose.yml.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
services:
redroid:
image: redroid/redroid:12.0.0_64only_mindthegapps
container_name: android-redroid
restart: unless-stopped
privileged: true
cpus: "4.0"
mem_limit: 8g
volumes:
- /home/ubuntu/android-redroid/data:/data
ports:
- "127.0.0.1:5555:5555"
networks:
- default
command:
- androidboot.redroid_width=1080
- androidboot.redroid_height=2280
- androidboot.redroid_dpi=420
- androidboot.redroid_fps=30
- androidboot.use_memfd=true
- androidboot.redroid_gpu_mode=guest
- ro.product.model=Samsung Galaxy S10
- ro.product.brand=samsung
- ro.product.manufacturer=samsung
- ro.product.device=beyond1
- ro.product.name=beyond1lte
android-novnc:
build:
context: ./novnc
image: local/android-novnc:latest
container_name: android-novnc
restart: unless-stopped
depends_on:
- redroid
networks:
- default
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.middlewares.android-https.redirectscheme.scheme=https"
- "traefik.http.routers.android.entrypoints=http"
- "traefik.http.routers.android.rule=Host(`android.example.com`)"
- "traefik.http.routers.android.middlewares=android-https"
- "traefik.http.routers.android-secure.entrypoints=https"
- "traefik.http.routers.android-secure.rule=Host(`android.example.com`)"
- "traefik.http.routers.android-secure.tls=true"
- "traefik.http.routers.android-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.android-secure.middlewares=authentik@docker"
- "traefik.http.routers.android-secure.service=android-vnc"
- "traefik.http.services.android-vnc.loadbalancer.server.port=6080"
android-vnc-tailnet:
image: alpine/socat:latest
container_name: android-vnc-tailnet
restart: unless-stopped
depends_on:
- redroid
command: "tcp-listen:5901,fork,reuseaddr tcp-connect:redroid:5901"
ports:
- "100.64.0.2:5901:5901"
networks:
- default
networks:
proxy:
external: true
Replace 100.64.0.2 with your server’s private tailnet IP. Remove android-vnc-tailnet entirely if you only want browser access.
Start Redroid
Run from /home/ubuntu/android-redroid:
1
2
cd /home/ubuntu/android-redroid
docker compose up -d redroid
Wait for Android to boot:
1
2
3
adb connect 127.0.0.1:5555
adb -s 127.0.0.1:5555 wait-for-device
adb -s 127.0.0.1:5555 shell getprop sys.boot_completed
Expected output:
1
1
Install droidVNC-NG into Android
Run:
1
adb -s 127.0.0.1:5555 install -r /home/ubuntu/android-redroid/vnc/droidvnc-ng.apk
Confirm package name:
1
adb -s 127.0.0.1:5555 shell pm list packages | grep droidvnc
Expected package:
1
package:net.christianbeier.droidvnc_ng
Enable droidVNC-NG permissions
Android remote-control apps usually need accessibility and screen-capture approval. For a server-side Redroid instance, preseed as much as possible with ADB.
Run:
1
2
3
4
5
adb -s 127.0.0.1:5555 shell settings put secure enabled_accessibility_services net.christianbeier.droidvnc_ng/.InputService
adb -s 127.0.0.1:5555 shell settings put secure accessibility_enabled 1
adb -s 127.0.0.1:5555 shell appops set net.christianbeier.droidvnc_ng PROJECT_MEDIA allow || true
Check accessibility:
1
adb -s 127.0.0.1:5555 shell settings get secure enabled_accessibility_services
Configure droidVNC-NG defaults
Create defaults inside Android:
1
adb -s 127.0.0.1:5555 shell mkdir -p /sdcard/Android/data/net.christianbeier.droidvnc_ng/files
Create /tmp/droidvnc-defaults.json on the host:
1
2
3
4
5
6
7
{
"port": 5901,
"password": "<strong-vnc-password>",
"scaling": 0.6,
"view_only": false,
"show_pointers": true
}
Push it into Android:
1
adb -s 127.0.0.1:5555 push /tmp/droidvnc-defaults.json /sdcard/Android/data/net.christianbeier.droidvnc_ng/files/defaults.json
Start droidVNC-NG service
Start the service with ADB:
1
adb -s 127.0.0.1:5555 shell am start-foreground-service -n net.christianbeier.droidvnc_ng/.MainService
Verify VNC responds from inside the Docker network:
1
docker run --rm --network android-redroid_default alpine:3.20 sh -lc 'apk add --no-cache busybox-extras >/dev/null && nc -vz redroid 5901'
Expected:
1
redroid (redroid:5901) open
You can also check the RFB handshake:
1
docker run --rm --network android-redroid_default alpine:3.20 sh -lc 'apk add --no-cache busybox-extras >/dev/null && printf "" | nc redroid 5901 | head -c 12'
Expected:
1
RFB 003.008
Start noVNC and tailnet VNC
Run:
1
2
cd /home/ubuntu/android-redroid
docker compose up -d --build
Check containers:
1
docker compose ps
Expected:
1
2
3
android-redroid
android-novnc
android-vnc-tailnet
Verify browser access
Open:
1
https://android.example.com
Expected browser flow:
1
Authentik login -> noVNC page -> VNC password prompt -> Android screen
If noVNC shows a WebSocket error, make sure the Traefik router sends all paths for android.example.com to android-novnc and that websockify listens on port 6080.
Verify native VNC over tailnet
From a device connected to your tailnet, connect a VNC client to:
1
100.64.0.2:5901
Use the VNC password from /home/ubuntu/android-redroid/secrets/droidvnc.env.
From the server, check the bind:
1
ss -tulpn | grep ':5901'
It should bind to the private tailnet IP, not public 0.0.0.0.
Add to Homepage dashboard
Edit /home/ubuntu/homepage/config/services.yaml.
1
2
3
4
5
6
- Remote Access:
- Android VNC:
icon: android.png
href: https://android.example.com
description: Browser VNC control for a Redroid Android instance
siteMonitor: https://android.example.com
Restart Homepage:
1
docker restart homepage
Google Play verification note
If Google Play accepts the email and password but asks for phone verification or “Verify it’s you,” that is a Google account security challenge. It is not a Redroid, GApps, noVNC, droidVNC-NG, or Traefik issue. The account owner must complete the verification flow.
Retiring the service while keeping docs
If you no longer need Android, keep this documentation and remove the live service.
Run:
1
2
3
cd /home/ubuntu/android-redroid
docker compose down --remove-orphans
rm -rf /home/ubuntu/android-redroid
Then remove the Homepage entry from /home/ubuntu/homepage/config/services.yaml and restart Homepage:
1
docker restart homepage
Troubleshooting
Redroid does not boot
Check logs:
1
2
3
docker logs --tail=200 android-redroid
adb connect 127.0.0.1:5555
adb -s 127.0.0.1:5555 shell getprop sys.boot_completed
If the host has no /dev/kvm, avoid KVM-based Android emulator images and use Redroid-compatible images instead.
noVNC connects but screen is black
Check droidVNC-NG inside Android:
1
adb -s 127.0.0.1:5555 shell dumpsys activity services net.christianbeier.droidvnc_ng | head -80
Make sure screen capture permission is active. If needed, open the droidVNC-NG app inside Android once and approve capture.
Keyboard or touch input does not work
Check accessibility:
1
2
adb -s 127.0.0.1:5555 shell settings get secure accessibility_enabled
adb -s 127.0.0.1:5555 shell settings get secure enabled_accessibility_services
The droidVNC-NG input service should be enabled.
Raw VNC is reachable publicly
This is unsafe. Fix the port bind in /home/ubuntu/android-redroid/docker-compose.yml:
1
2
ports:
- "100.64.0.2:5901:5901"
Do not use:
1
2
ports:
- "5901:5901"
The second form binds on public interfaces.
Security checklist
- noVNC route protected by Authentik.
- VNC password set.
- Raw VNC bound to private tailnet IP only.
- ADB bound to
127.0.0.1only. - No public
0.0.0.0:5901listener. - Homepage entry removed when service is retired.
- Android data directory deleted when no longer needed.