Android Complete Guide
A complete setup and diagnosis guide for using a rooted arm64 Android device as a Docker host.
Vyline does not include a native Android Docker-host feature. This guide covers the special case of treating an Android device with root access and container-capable kernel features as a Linux host. Normal Linux servers and Raspberry Pi systems do not need these steps.
Scope
The Android documentation separates two kinds of information.
| Type | What it covers |
|---|---|
| Common requirements | arm64, root, namespaces/cgroups/seccomp, filesystem and network features required by Docker, real chroot, and persistence |
| Conditional workarounds | ext4 loop fallback when overlay2 fails on F2FS, access-path diagnosis, Android netd/policy-routing adjustments, legacy iptables DNAT, and similar fixes |
The second group depends on the device, ROM, kernel, and Android version. Do not apply those workarounds to a system that does not show the corresponding symptom.
How to use this guide
When Android is used as a Docker host, make decisions from the actual kernel features, filesystem, mount propagation, cgroup layout, netfilter backend, and Android routing rules—not from the device model name. Even devices with the same SoC or Android version can behave differently because the vendor kernel, custom kernel, or ROM differs.
The workarounds below generalize failures and recoveries reproduced on real hardware. Apply a workaround only when the symptom matches, and avoid changing layers that are already working.
Layout
Android arm64
├─ Android OS / vendor kernel
│ ├─ root
│ ├─ namespaces / cgroups / seccomp
│ ├─ OverlayFS / ext4 / loop
│ └─ veth / bridge / netfilter
├─ Termux
│ └─ rootfs download / SSH / startup entry point
├─ Linux rootfs
│ └─ real chroot
├─ Docker Engine / containerd / runc
└─ Vyline
├─ /app/data
└─ /app/storageproot-distro is convenient for obtaining an Ubuntu rootfs, but it is not the layer where the Docker daemon should run long-term. Docker and runc use kernel namespaces, mounts, cgroups, and devices directly, so runtime operation uses a real chroot as root.
1. Preflight
uname -a
uname -m
id
getenforce
cat /proc/cgroups
mount | grep -E 'cgroup|cgroup2'
ls -l /dev/block/loop* 2>/dev/nulluname -m is expected to return aarch64. This configuration cannot work on a device where you cannot obtain a root shell.
At the kernel level, check at least namespaces, cgroups, seccomp, OverlayFS, veth, bridge, netfilter/conntrack/NAT, ext4, and loop devices. The detailed CONFIG checklist is in Android Kernel.
Classify the device into one of four states first
| State | Interpretation | Next step |
|---|---|---|
docker run --rm hello-world works | The basic container runtime is working | Move on to networking and persistence |
| dockerd does not start | Kernel/cgroup/seccomp/storage problem | Check dockerd --debug and kernel config |
| Containers start but cannot reach the Internet | Android routing/netfilter problem | Check ip rule, FORWARD, and MASQUERADE |
Outbound traffic works but LAN cannot reach -p | Published-port packet-path mismatch | Check nft/legacy, DNAT, and bridge FORWARD |
2. Termux and the rootfs
pkg update -y
pkg install -y proot-distro busybox openssh tmux coreutils curl
proot-distro install ubuntuA typical rootfs location is under Termux's var/lib/proot-distro/containers/ubuntu/rootfs. The actual path is referred to as ROOT below.
PREFIX=/data/data/com.termux/files/usr
ROOT="$PREFIX/var/lib/proot-distro/containers/ubuntu/rootfs"
BB="$PREFIX/bin/busybox"proot-distro login ubuntu is fine for initial rootfs setup, but do not use it as the normal dockerd/runc path. proot translates syscalls in user space and therefore cannot expose mount namespaces, cgroups, devices, and propagation in the same way Docker requires.
3. Mounts for the real chroot
Simply running chroot "$ROOT" is not always enough for runc. Some Android kernel/mount layouts require turning the rootfs into a mountpoint and marking it rslave so child mounts do not propagate back to the host.
$BB mount --bind "$ROOT" "$ROOT"
$BB mount --make-rslave "$ROOT"
$BB mount --rbind /dev "$ROOT/dev"
$BB mount --make-rslave "$ROOT/dev"
$BB mount -t proc proc "$ROOT/proc"
$BB mount --rbind /sys "$ROOT/sys"
$BB mount --make-rslave "$ROOT/sys"
$BB mount -t tmpfs -o mode=755,nosuid,nodev tmpfs "$ROOT/run"If runc fails with an error such as remount / ... invalid argument, inspect this mount layout first. On devices that already work, do not add unnecessary remount operations.
/dev/pts and DNS
When /dev is --rbind-mounted, /dev/pts is normally visible as well. Some devices still have incomplete PTY or DNS behavior; verify the following from inside the chroot.
ls -ld /dev/pts /proc /sys /run
cat /etc/resolv.conf
getent hosts download.docker.com
mount | grep -E "$ROOT|/var/lib/docker"If only DNS fails, provide a resolver in $ROOT/etc/resolv.conf that actually works on the Android host. Do not permanently hard-code a public DNS server without verifying name resolution again after changing Wi-Fi or VPN connections.
4. SELinux
If Docker works while SELinux remains enforcing, leave it that way. If loop devices, mounts, dockerd, or iptables operations are denied, inspect audit logs and identify the exact cause.
setenforce 0 is not a general requirementIf the symptom disappears in permissive mode, SELinux becomes a likely cause, but disabling enforcement weakens the entire Android security boundary. After diagnosis, inspect the denied operations in audit logs and add only the required sepolicy when possible.
5. Docker data-root
First test whether Docker's storage driver works on the current filesystem. Only when Android /data uses a configuration such as F2FS/casefold that produces EINVAL while creating overlay2 layers should you fall back to an ext4 loop image.
findmnt -T /var/lib/docker 2>/dev/null || true
stat -f -c '%T' /data
docker info 2>/dev/null | grep -E 'Storage Driver|Backing Filesystem|Supports d_type'F2FS does not fail in every configuration. Use the fallback only when creating overlay2 layers or starting containers actually produces invalid argument/EINVAL.
Example ext4-loop fallback:
mkdir -p /data/local/docker-storage
truncate -s 64G /data/local/docker-storage/docker-ext4.img
/system/bin/mke2fs -t ext4 -F /data/local/docker-storage/docker-ext4.img
LOOP=$(/system/bin/losetup -f)
/system/bin/losetup "$LOOP" /data/local/docker-storage/docker-ext4.img
$BB mount -t ext4 -o rw,noatime "$LOOP" "$ROOT/var/lib/docker"64 GB is only an example. Size it according to free space on the device and the amount of media Vyline will retain. If overlay2 works correctly on F2FS, the loop image is unnecessary.
The loop image is a single file stored on Android userdata. Do not size it beyond available capacity, assume a fixed loop number, or ignore power-loss risk. After reboot, obtain a free device again with losetup -f, and avoid attaching the same image twice if it is already attached.
6. Docker Engine
Inside the Ubuntu rootfs, install Engine, containerd, and the Compose plugin from Docker's normal Ubuntu repository. Because systemd is not PID 1 in this setup, start dockerd directly rather than relying on systemctl start docker.
apt update
apt install -y ca-certificates curl gnupg iproute2 iptables procps kmod util-linux coreutilsapt update
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${UBUNTU_CODENAME:-$VERSION_CODENAME} stable" \
> /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Daemon configuration without assuming systemd
In a real chroot where Android init remains PID 1, using Ubuntu systemd as the Docker service manager adds complexity. Inspect cgroup mounts first; when a systemd cgroup driver is not viable, explicitly use cgroupfs.
mkdir -p /etc/docker
cat >/etc/docker/daemon.json <<'EOF'
{
"storage-driver": "overlay2",
"exec-opts": ["native.cgroupdriver=cgroupfs"],
"log-driver": "local"
}
EOFIf another daemon.json already exists, merge settings instead of overwriting it. On a device where cgroup v2 is exposed correctly and another driver is already stable, there is no reason to force cgroupfs.
{
"storage-driver": "overlay2",
"exec-opts": ["native.cgroupdriver=cgroupfs"],
"log-driver": "local"
}This is a reference configuration for Android chroots. Devices with a different cgroup layout should be adjusted based on daemon logs.
rm -f /var/run/docker.pid
nohup dockerd >/var/log/dockerd.log 2>&1 </dev/null &
docker version
docker info
docker run --rm hello-world
docker run --rm alpine uname -mIn docker info, inspect Storage Driver, Backing Filesystem, Cgroup Driver/Version, and Architecture. On an arm64 host, confirm Architecture: aarch64 and the storage/cgroup configuration you intended.
Do not proceed to Vyline until hello-world works. A failure at this stage belongs to the Android host or Docker layer, not Vyline.
What to look for in dockerd logs
tail -n 200 /var/log/dockerd.log
docker info
cat /proc/self/cgroup
mount | grep -E 'cgroup|overlay|/var/lib/docker'Prioritize storage for failed to mount overlay, kernel/cgroup for cgroup/devices/pids failures, and netfilter/iptables for failed to create NAT chain.
7. Docker network
Next, test outbound container traffic separately from host port publishing.
docker run --rm alpine ping -c 1 1.1.1.1
docker run --rm -p 8080:80 nginx:alpineIf both behave like normal Linux, no extra networking work is required. Only when they fail should you investigate Android netd, policy routing, ip_forward, FORWARD, MASQUERADE, and the iptables backend.
On some Android devices, Docker inside the chroot creates nft rules while the real packet path traverses Android-side legacy iptables, breaking only published ports. In that case, add DNAT/FORWARD on the actual Android packet path. Interface names, routing tables, iptables backends, and chain names differ by device, so do not copy fixed values; follow the diagnosis sequence in Android Networking.
ip -br addr
ip rule
ip route show table main
ip route get 1.1.1.1
cat /proc/sys/net/ipv4/ip_forward
iptables --version
iptables -S FORWARD
iptables -t nat -S
docker network ls
docker network inspect bridge
8. Start Vyline
After Docker itself is healthy, use the same Compose configuration as a normal arm64 Linux host. The GHCR image includes linux/arm64.
mkdir -p /opt/vyline
cd /opt/vyline
curl -LO https://raw.githubusercontent.com/tqmane/vyline/main/docker-compose.yml
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100Compose mounts ./data at /app/data and ./storage at /app/storage. Do not delete these directories during updates.
curl http://127.0.0.1:3000/healthzFor LAN access, the Compose host port defaults to 0.0.0.0:3000. Even with VYLINE_LAN_ACCESS=false, non-loopback deployment requires subdevice authentication for remote requests. See Access Model for the exact meaning of these settings.
Verify persistence before using the service
mkdir -p /opt/vyline/data /opt/vyline/storage
touch /opt/vyline/data/.write-test /opt/vyline/storage/.write-test
docker inspect vyline --format '{{json .Mounts}}'
df -h /opt/vyline/data /opt/vyline/storageDiscovering a mount error only after login, restore, and media storage can lose state. Confirm first that the same host paths remain mounted after recreating the container.
9. Portainer is optional
Vyline does not require Portainer. Add it as a management UI only after Docker CLI/Compose is stable. On Android chroots where bridge publishing is broken, using host networking for Portainer alone can be a useful diagnostic workaround, but that is not the default for normal Linux.
Cloudflare Tunnel, Tailscale, and WireGuard are also optional. They are unnecessary for LAN-only use and should be added only when remote access requires an authenticated path.
10. Automatic startup
If you automate startup, keep the order explicit.
1. obtain root
2. mount the chroot rootfs
3. attach/mount the ext4 loop image if needed
4. apply Android network corrections if needed
5. start dockerd
6. wait until docker info is ready
7. docker compose up -dImmediately after Android boot, data mounts and network interfaces may not be ready. Waiting for the required mount, interface, and Docker socket to actually appear is more reliable than sleeping for a fixed number of seconds.
The same applies to Termux:Boot: do not run dockerd immediately just because the boot hook fired. Gate startup on /data, rootfs, loop mount, and network-interface readiness. On a dedicated server device that Doze suspends for long periods, consider battery-optimization exclusion or a Termux wake lock only as needed for server operation.
11. Diagnose by symptom
| Symptom | Check first |
|---|---|
overlay2 ... invalid argument | Filesystem backing Docker data-root; consider ext4 loop only when F2FS/casefold is the cause |
remount / ... invalid argument | Rootfs self-bind and mount propagation |
| Container cannot reach the Internet | ip rule, routes, FORWARD, MASQUERADE, netd |
-p 3000:3000 resets from LAN | Docker iptables backend versus the Android packet path |
Only docker exec sees a broken filesystem | Compare /proc/<container-pid>/root with the exec namespace |
| Restored state disappears after Vyline reload | /app/data / /app/storage mounts and write permissions |
When only docker exec is broken
On some Android + real-chroot combinations, the container itself is healthy while only processes launched through exec see a different mount view. Do not conclude that the container's /app disappeared based only on docker exec.
CID=$(docker inspect -f '{{.State.Pid}}' vyline)
readlink /proc/$CID/ns/mnt
ls -la /proc/$CID/root/app
nsenter -t "$CID" -m -- ls -la /app
docker exec vyline ls -la /appIf /proc/$PID/root and nsenter are correct while only docker exec is abnormal, suspect the runtime/chroot exec path. For the same reason, if only healthchecks fail, verify HTTP from the host first; disabling the Compose healthcheck is then an available workaround.
For Vyline-specific symptoms, continue to Troubleshooting; for Android kernel issues use Kernel; for networking use Android Networking.
12. Security
- Using a rooted Android device as a server increases the attack surface compared with a normal mobile device.
- If SELinux permissive is required, do not treat the device as having the same trust level as a daily-use phone.
- Never expose the Docker socket, Portainer, or Vyline directly to the Internet without authentication.
- Do not leak
data/,storage/, LINE sessions/tokens, or backups. - When remote access is required, place an authenticated boundary such as Tailscale/WireGuard, Cloudflare Access + Tunnel, or an authenticated reverse proxy in front of it.
Required, conditional, and optional
| Element | Status |
|---|---|
| arm64 / root / container-capable kernel features | Required |
| real chroot | Required for this setup |
| GHCR arm64 Vyline image | Normally use this |
| ext4 loop image | Only when overlay2 fails on F2FS or another backing filesystem |
| SELinux permissive | Conditional diagnostic workaround; not a general requirement |
| Manual route / iptables corrections | Only when Android networking actually fails |
| Portainer | Optional |
| Cloudflare Tunnel / Tailscale | Only when remote access is needed |
| KVM | Not required by Vyline |