Vyline DOCS

Android Complete Guide

A complete setup and diagnosis guide for using a rooted arm64 Android device as a Docker host.

This is not a normal Android installation

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.

TypeWhat it covers
Common requirementsarm64, root, namespaces/cgroups/seccomp, filesystem and network features required by Docker, real chroot, and persistence
Conditional workaroundsext4 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/storage

proot-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/null

uname -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

StateInterpretationNext step
docker run --rm hello-world worksThe basic container runtime is workingMove on to networking and persistence
dockerd does not startKernel/cgroup/seccomp/storage problemCheck dockerd --debug and kernel config
Containers start but cannot reach the InternetAndroid routing/netfilter problemCheck ip rule, FORWARD, and MASQUERADE
Outbound traffic works but LAN cannot reach -pPublished-port packet-path mismatchCheck 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 ubuntu

A 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 requirement

If 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 coreutils
apt 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"
}
EOF

If 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 -m

In 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:alpine

If 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=100

Compose mounts ./data at /app/data and ./storage at /app/storage. Do not delete these directories during updates.

curl http://127.0.0.1:3000/healthz

For 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/storage

Discovering 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 -d

Immediately 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

SymptomCheck first
overlay2 ... invalid argumentFilesystem backing Docker data-root; consider ext4 loop only when F2FS/casefold is the cause
remount / ... invalid argumentRootfs self-bind and mount propagation
Container cannot reach the Internetip rule, routes, FORWARD, MASQUERADE, netd
-p 3000:3000 resets from LANDocker iptables backend versus the Android packet path
Only docker exec sees a broken filesystemCompare /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 /app

If /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

Required, conditional, and optional

ElementStatus
arm64 / root / container-capable kernel featuresRequired
real chrootRequired for this setup
GHCR arm64 Vyline imageNormally use this
ext4 loop imageOnly when overlay2 fails on F2FS or another backing filesystem
SELinux permissiveConditional diagnostic workaround; not a general requirement
Manual route / iptables correctionsOnly when Android networking actually fails
PortainerOptional
Cloudflare Tunnel / TailscaleOnly when remote access is needed
KVMNot required by Vyline
Search by page, setting, or error