Vyline DOCS

Android 完整指南

将已 root 的 arm64 Android 设备作为 Docker 主机时的完整结构、安装和故障排查流程。

这不是普通的 Android 安装方式

Vyline 没有原生 Android Docker-host 功能。本页处理的是一种特殊方案:把具有 root 权限和 container 所需内核能力的 Android 设备当作 Linux 主机使用。普通 Linux 服务器或 Raspberry Pi 不需要这些步骤。

适用范围

Android 文档把信息分成两类。

类别内容
通用要求arm64、root、namespaces/cgroups/seccomp、Docker 所需 filesystem/network 功能、real chroot、持久化
条件式规避方案F2FS 上 overlay2 失败时的 ext4 loop、访问路径诊断、Android netd/policy routing 修正、legacy iptables DNAT 等

第二类会随设备、ROM、kernel 和 Android 版本变化。没有出现对应症状的环境,不要一律套用这些修正。

本指南的判断方式

把 Android 作为 Docker 主机时,应根据实际的 kernel 功能、filesystem、mount propagation、cgroup 结构、netfilter backend 和 Android routing rule 判断,而不是根据设备型号。即使 SoC 和 Android 版本相同,vendor kernel、custom kernel 或 ROM 不同也可能得到不同结果。

下面的规避方案是把实机上复现过的失败和恢复方法抽象为通用步骤。只有症状一致时才应用,不要对已经正常工作的层加入多余修改。

结构

Android arm64
├─ Android OS / vendor kernel
│  ├─ root
│  ├─ namespaces / cgroups / seccomp
│  ├─ OverlayFS / ext4 / loop
│  └─ veth / bridge / netfilter
├─ Termux
│  └─ 获取 rootfs / SSH / 启动入口
├─ Linux rootfs
│  └─ real chroot
├─ Docker Engine / containerd / runc
└─ Vyline
   ├─ /app/data
   └─ /app/storage

proot-distro 很适合获取 Ubuntu rootfs,但不要把它作为长期运行 Docker daemon 的层。Docker/runc 直接使用内核 namespace、mount、cgroup 和 device,因此实际运行时应以 root 使用 real chroot。

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 预期为 aarch64。无法取得 root shell 的设备无法使用此方案。

内核侧至少要检查 namespace、cgroup、seccomp、OverlayFS、veth、bridge、netfilter/conntrack/NAT、ext4、loop device。具体 CONFIG 请看 Android 内核

先把设备分成四种状态

状态判断下一步
docker run --rm hello-world 能运行container runtime 基础成立继续检查网络和持久化
dockerd 无法启动kernel/cgroup/seccomp/storage 问题检查 dockerd --debug 与 kernel config
container 能启动但不能出网Android routing/netfilter 问题检查 ip rule / FORWARD / MASQUERADE
能出网但 LAN 无法访问 -ppublished-port packet path 不一致检查 nft/legacy、DNAT、bridge FORWARD

2. Termux 与 rootfs

pkg update -y
pkg install -y proot-distro busybox openssh tmux coreutils curl
proot-distro install ubuntu

标准 rootfs 通常位于 Termux 的 var/lib/proot-distro/containers/ubuntu/rootfs 下。以下把实际路径记为 ROOT

PREFIX=/data/data/com.termux/files/usr
ROOT="$PREFIX/var/lib/proot-distro/containers/ubuntu/rootfs"
BB="$PREFIX/bin/busybox"

proot-distro login ubuntu 可用于 rootfs 初始设置,但不要用作 dockerd/runc 的常规运行路径。proot 在 userland 转换 syscall,无法按 Docker 所需方式直接处理 mount namespace、cgroup、device 和 propagation。

3. real chroot 的 mount

单纯执行 chroot "$ROOT" 并不一定能让 runc 工作。部分 Android kernel/mount 结构需要先让 rootfs 成为 mountpoint,并设为 rslave,避免子 mount 反向传播到 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"

如果 runc 出现 remount / ... invalid argument 一类错误,先检查这套 mount 结构。已经正常工作的设备不要额外增加不必要的 remount。

/dev/pts 与 DNS

/dev 使用 --rbind 后,通常也能看到 /dev/pts。部分设备只有 PTY 或 DNS 不完整,请在 chroot 内检查:

ls -ld /dev/pts /proc /sys /run
cat /etc/resolv.conf
getent hosts download.docker.com
mount | grep -E "$ROOT|/var/lib/docker"

如果只有 DNS 失败,请在 $ROOT/etc/resolv.conf 中使用 Android 主机实际可用的 resolver。不要把某个 public DNS 固定为长期配置而不验证 Wi-Fi 或 VPN 切换后的名称解析。

4. SELinux

如果 SELinux enforcing 下 Docker 能正常运行,就保持 enforcing。loop device、mount、dockerd 或 iptables 操作被拒绝时,先检查 audit log 确认原因。

setenforce 0 不是通用要求

如果 permissive 后症状消失,可以把 SELinux 列为原因候选,但这会降低整个 Android 的防护。完成定位后应从 audit log 确认拒绝对象,并尽可能只加入必要的 sepolicy。

5. Docker data-root

先确认 Docker storage driver 能否在当前 filesystem 上正常工作。只有 Android /data 的 F2FS/casefold 等组合在创建 overlay2 时实际出现 EINVAL,才需要切到 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 并不必然失败。只有在实际创建 overlay2 layer 或启动 container 时出现 invalid argument/EINVAL 才使用 fallback。

切换到 ext4 loop 的示例:

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 只是示例,应根据设备剩余空间和 Vyline 保存媒体的规模决定。如果 F2FS 上 overlay2 正常,这套 loop 配置就不是必需的。

loop image 是 Android userdata 上的单一文件。不要设定超过设备可用空间的容量,不要假定固定 loop 编号,也要考虑突然断电风险。重启后用 losetup -f 重新获取空闲设备;已经 attach 时不要重复 attach。

6. Docker Engine

在 Ubuntu rootfs 内,从 Docker 的常规 Ubuntu repository 安装 Engine、containerd 和 Compose plugin。由于该方案中 systemd 不是 PID 1,应直接启动 dockerd,而不是依赖 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

不假定 systemd 的 daemon 配置

在 Android init 仍是 PID 1 的 real chroot 中,让 Ubuntu systemd 充当 Docker service manager 会增加复杂度。先检查 cgroup mount;systemd driver 不成立时再显式使用 cgroupfs

mkdir -p /etc/docker
cat >/etc/docker/daemon.json <<'EOF'
{
  "storage-driver": "overlay2",
  "exec-opts": ["native.cgroupdriver=cgroupfs"],
  "log-driver": "local"
}
EOF

已有其他 daemon.json 时不要直接覆盖,应合并设置。如果设备正确暴露 cgroup v2,并且使用其他 driver 已经稳定,也不必强制改成 cgroupfs

{
  "storage-driver": "overlay2",
  "exec-opts": ["native.cgroupdriver=cgroupfs"],
  "log-driver": "local"
}

这是 Android chroot 的参考设置。cgroup 结构不同的设备应根据 daemon log 调整。

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

docker info 中检查 Storage Driver、Backing Filesystem、Cgroup Driver/Version 与 Architecture。arm64 host 应显示 Architecture: aarch64,并与预期的 storage/cgroup 配置一致。

hello-world 没有通过前不要继续安装 Vyline。此处失败属于 Android host / Docker 层,而不是 Vyline。

dockerd log 的重点

tail -n 200 /var/log/dockerd.log
docker info
cat /proc/self/cgroup
mount | grep -E 'cgroup|overlay|/var/lib/docker'

failed to mount overlay 优先查 storage;cgroup/devices/pids 优先查 kernel/cgroup;failed to create NAT chain 优先查 netfilter/iptables。

7. Docker network

接下来把 container 出网与 host port publish 分开测试。

docker run --rm alpine ping -c 1 1.1.1.1
docker run --rm -p 8080:80 nginx:alpine

如果和普通 Linux 一样正常,就无需额外配置。只有失败时才检查 Android netd、policy routing、ip_forward、FORWARD、MASQUERADE 和 iptables backend。

部分 Android 中,chroot 内 Docker 创建 nft rule,但真实 packet path 经过 Android 侧 legacy iptables,因此只有 published port 失效。此时需要在 Android 实际路径上补充 DNAT/FORWARD。interface 名、routing table、iptables backend 和 chain 名都会因设备不同,不要复制固定值;请按 Android 网络 的诊断顺序确认。

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. 启动 Vyline

Docker 本身正常后,可以使用与普通 arm64 Linux host 相同的 Compose。GHCR image 提供 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 会把 ./data mount 到 /app/data,把 ./storage mount 到 /app/storage。更新时不要删除这两个目录。

curl http://127.0.0.1:3000/healthz

LAN 访问时,Compose host port 默认是 0.0.0.0:3000。即便保持 VYLINE_LAN_ACCESS=false,非 loopback 部署也会要求 remote request 进行 subdevice 认证。设置含义请看 访问模型

先验证持久化

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

登录、恢复和媒体保存之后才发现 mount 错误,可能导致状态丢失。先确认 container 重建后仍然 mount 同一 host path。

9. Portainer 是可选项

Vyline 启动不需要 Portainer。先让 Docker CLI / Compose 稳定,再把它作为管理 UI 添加。Android chroot 中如果 bridge publish 损坏,只让 Portainer 使用 host network 可能是有效的诊断规避方案,但这不是普通 Linux 的默认配置。

Cloudflare Tunnel、Tailscale 和 WireGuard 同样是可选项。只在 LAN 内使用时不需要;只有需要远程访问时才作为认证访问路径加入。

10. 自动启动

自动启动时固定顺序。

1. 取得 root
2. mount chroot rootfs
3. 必要时 attach/mount ext4 loop
4. 必要时应用 Android network 修正
5. 启动 dockerd
6. 等待 docker info ready
7. docker compose up -d

Android 刚启动时 data mount 和 network interface 可能尚未就绪。与其固定等待若干秒,不如确认所需 mount/interface/docker socket 实际出现后再进入下一步。

Termux:Boot 也一样,不要“触发后立即 dockerd”。应以 /data、rootfs、loop mount 和 network interface 准备完成为条件。专用服务器设备如果被 Doze 长时间挂起,可按需要考虑 Termux battery optimization 豁免或 wake lock。

11. 按症状排查

症状首先检查
overlay2 ... invalid argumentDocker data-root 的 filesystem;确认 F2FS/casefold 是原因时再考虑 ext4 loop
remount / ... invalid argumentchroot rootfs self-bind / mount propagation
container 无法访问 Internetip rule、route、FORWARD、MASQUERADE、netd
-p 3000:3000 从 LAN 访问 resetDocker 使用的 iptables backend 与 Android 真实 packet path
只有 docker exec 的 filesystem 异常比较 /proc/<container-pid>/root 与 exec namespace
Vyline reload 后恢复状态消失/app/data / /app/storage mount 与写权限

只有 docker exec 损坏时

部分 Android + real-chroot 组合中,container 本体正常,但通过 exec 启动的 process 看到不同的 mount view。不要仅凭 docker exec 就判断 container 的 /app 消失了。

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

如果 /proc/$PID/rootnsenter 正常,只有 docker exec 异常,应怀疑 runtime/chroot 的 exec 路径。同理,如果只有 healthcheck 失败,先从 host 验证 HTTP;确认本体正常后可以考虑禁用 Compose healthcheck。

Vyline 特有问题请继续看 故障排查;Android kernel 问题看 内核;网络问题看 Android 网络

12. 安全

必需、条件式、可选

要素处理方式
arm64 / root / container 所需 kernel 功能必需
real chroot此方案必需
GHCR arm64 Vyline image通常使用它
ext4 loop image只有 F2FS 等 backing filesystem 上 overlay2 失败时
SELinux permissive用于定位原因的条件式规避方案;不是通用要求
手动 route / iptables 修正只有 Android network 实际失败时
Portainer可选
Cloudflare Tunnel / Tailscale只有需要远程访问时
KVMVyline 不需要
按页面、设置或错误搜索