Podroid:免 root 在安卓手机上跑 Docker 和完整 Linux 桌面


本文目录

在手机上跑 Linux,过去的标准答案是 Termux 加 proot:装包、写脚本、跑个 Python 服务都没问题,但它和安卓共用一个内核,没有完整的 cgroup 和 namespace,Docker 基本不用想。

Podroid 换了个思路:不翻译、不 chroot,直接在应用里启动一台带独立内核的 Alpine Linux 虚拟机。客户机看到的是干净的 Linux 环境,于是 Podman、Docker、LXC 的行为和服务器上完全一致。

这个项目 2026 年 3 月才建仓,到 11 月已经拿到 3.8k star,迭代节奏基本是每月一版。本文依据 v1.3.0(2026-11-09 发布) 的仓库、发布说明和官方文档整理;文中的性能数字来自作者在两台手机上的实测,不是我的跑分,也不是对你手机的承诺。

一、基本信息

项目内容
仓库ExTV/Podroid
官网与文档extv.github.io/Podroid
许可证GPLv2
安卓端语言Kotlin
最新版本v1.3.0(2026-11-09)
系统要求Android 8(API 26)及以上,仅 arm64-v8a
分发方式GitHub Releases 侧载 APK,没有 Play 商店版本
APK 体积约 310 MiB(325 MB,内含系统镜像)
客户机Alpine Linux 3.24 + 定制 Linux 7.3 内核,QEMU 11.1.2
默认凭据root / podroid,SSH 端口 9922
界面语言英文、中文

两个门槛要先看清:只有 arm64,32 位 ARM 和 x86 设备装不上(我那台玩客云就是 32 位 ARM,无缘);Android 8 起步,因为虚拟机依赖的 SELinux/seccomp 策略和原生 ABI 从 API 26 才齐。

二、它凭什么是”真”虚拟机

Podroid 的文件系统分两层,这也是它和 proot 方案最根本的区别:

  • /dev/vdb:只读 squashfs,完整的 Alpine 基础系统(软件包、OpenRC 服务、配置)压缩后打包在 APK 里,永不写入,升级基础系统靠发新 APK。
  • /dev/vda:可写 ext4 镜像,也就是安装向导里让你选大小的那个持久化 overlay。apk add 装的东西、容器镜像、SSH 主机密钥、home 目录全落在这里,重启和覆盖安装都还在。

一个极简 initramfs 先启动,把两块设备用 overlayfs 叠起来(squashfs 作 lower、ext4 作 upper),然后 switch_root 交给真正的 Alpine 根。

为什么是 switch_root 而不是 chroot? 早期版本用过 chroot,结果 podman exec -it 坏了:crun 调 setns(MNT) 进容器命名空间时会重置根,exec 进去的进程看到的是 /mnt/overlay/proc 这种原始内核路径而不是 /proc。switch_root 直接重排内核挂载树,命名空间分叉后才能始终看到干净的 /。这个细节很能说明作者是真在跑容器,而不是只做了个能开机的壳。

客户机里 OpenRC 管服务,几个关键服务:

服务作用
podroid-bootstrap启用 cgroup v2、加载内核模块、挂载 devpts/shm/mqueue、配置 ZRAM swap、设置 sysctl 与主机名,把持久化 ext4 绑到 Podman/Docker/LXC 的数据目录
podroid-network拉起网卡(QEMU/UML 是 eth0/vec0,10.0.2.15/24;AVF 走 DHCP),写 /etc/resolv.conf
podroid-hostd客户机到安卓的桥接守护进程,podroid-notify、podroid-forward 等命令靠它回传
podroid-x11启动 Xvnc(display :0,端口 5900)和 PulseAudio(4713),支撑内置桌面查看器
podroid-ready最后运行,打出 Starting SSH... / Almost ready... / Ready! 标记,App 靠它判断启动成功

容器运行时是预装且开机即用的:Podman 无守护进程,直接能跑;Docker 和 LXC 已加入 default runlevel,每次开机自动启动。用 rc-status 确认,只用 Podman 的话可以 rc-service docker stop 或 rc-update del docker default 把内存省下来。

安卓侧则用一个前台服务持有 partial WakeLock,保证切到后台时 CPU 不被调度器停掉——通知栏那条常驻通知就是它的状态显示和停止入口。

三、三种后端:决定快慢的关键

同一套客户机镜像,底下可以换三种虚拟化层。这是 Podroid 最需要理解的一件事,体验差异几乎全在这里。

QEMU (TCG)AVF (pKVM)UML(实验性)
原理软件逐条模拟客户机指令安卓虚拟化框架,跑在物理核上客户机内核作为一个普通安卓进程运行
设备要求任意 arm64 + Android 8+厂商带 pKVM(目前主要是 Pixel 8 及以后)任意 arm64 + Android 8+
额外授权无两条一次性 pm grant无
CPU 速度比原生慢数倍接近原生明显快于 QEMU,仍不及 AVF
USB 直通支持(需开机前启用)不支持不支持
端口转发TCP + UDPTCP + UDPTCP + UDP
默认选中Auto 的兜底Auto 优先选它Auto 永不选它

Auto 的判定有四个条件:存在 android.software.virtualization_framework 特性、MANAGE_VIRTUAL_MACHINE 已授权、USE_CUSTOM_VIRTUAL_MACHINE 已授权、设备声明支持非受保护虚拟机(non-protected VM)。四个全过才用 AVF,否则回落 QEMU,并在首页、Status 页和设置里说明原因。

第四个条件最容易被忽略:pKVM 有 protected 和 non-protected 两种模式,Podroid 的客户机是普通 Alpine squashfs,满足不了受保护虚拟机的签名与度量要求。只支持 protected VM 的设备用不了 AVF,而且没有应用侧的绕法——pvmfw 只引导带签名验证启动镜像的载荷。剩下的路只有 root 后直接打开 /dev/kvm(高通上是 /dev/gunyah),而 Podroid 坚持免 root,所以这类设备就老老实实跑 QEMU。

启用 AVF

先确认设备支持:

adb shell pm list features | grep virtualization_framework

打印出 android.software.virtualization_framework 才有戏。然后授权这两个开发模式权限(系统 UI 里不暴露):

adb shell pm grant com.excp.podroid android.permission.MANAGE_VIRTUAL_MACHINE
adb shell pm grant com.excp.podroid android.permission.USE_CUSTOM_VIRTUAL_MACHINE

手边没电脑可以用 Shizuku:启动后在它的 rish shell 里直接跑同样的 pm grant 命令。最后在 App 里走一遍 Settings → About → AVF (pKVM) diagnostic,它会检查特性标志、两个权限、/apex/com.android.virt、AVF 系统服务和内核上报的 hypervisor 能力。

这两条授权跨重启和覆盖安装保留,但卸载重装后失效,需要重新 grant。

启用 UML

v1.3.0 新增的第三条路,思路很巧:把客户机 Linux 内核当成一个普通安卓进程跑,客户机代码就直接在 CPU 上执行,不再逐条模拟。不需要 root,不需要 adb 授权,任意 arm64 + Android 8+ 都能试。

开启方式:停止虚拟机 → Settings → Show advanced → Backend → UML (experimental) → 启动。首次启动会跑一个几秒钟的兼容性测试(含 DNS 查询,必须联网),通过后正常引导。首页会多出一行 Backend 显示 UML (user-mode),终端里 uname -r 也会变成另一个内核版本。

测试失败、UML 崩溃或 120 秒内没启动完,Podroid 会在这台设备上关掉 UML,改用 Auto 会选的后端启动,并弹通知说明原因,设置里给出 Try UML again 按钮。数据在所有后端共用同一块磁盘,来回切换不丢东西。

代价也要清楚:

  • 没有 USB 直通,需要 USB 设备就得回 QEMU。
  • 客户机里每个进程都是真实的安卓进程,包括每个容器进程。Android 12+ 的 phantom-process 限制会杀进程,容器一多就容易中招;Android 14+ 打开开发者选项里的 Disable child process restrictions,12/13 见下文排错。
  • 它不是安全边界。客户机内核以应用自身权限运行,别指望 VM 级隔离,不要在里面跑不可信代码。
  • 关机更慢,干净关闭约 15–30 秒;网卡名是 vec0 而不是 eth0;APK 体积增加约 13 MB。
  • 作者只在两台手机上测过,16 KB 内存页设备和 Android 8–11 的老内核完全没测。

性能预期

作者在 v1.3.0 发布说明里给了两组实测。第一组是 Nothing Phone (1)、Android 15、无 AVF,同一构建、同一磁盘、4 CPU / 2 GB RAM,三次取中位数:

测试QEMUUML
启动到 Ready33–41 s13–18 s
npm install express(热缓存)49.5 s24.8 s
Node.js CPU 循环(3 亿次)18.8 s4.7 s
docker run 内含 shell 循环19.7 s7.4 s
apk add nodejs npm(含下载)21.2 s12.4 s
docker run --rm alpine true(仅容器启动)7.6 s8.2 s

第二组是 Pixel 10 Pro XL、Android 17,三种后端同场对比:

测试QEMUUMLAVF
启动到 Ready44 s36 s10 s
npm install express(热缓存)25.7 s20.6 s1.9 s
Node.js CPU 循环10.5 s3.4 s0.9 s
docker run --rm alpine true9.1 s8.4 s0.85 s

读出来的结论:算力密集的活收益最大(约 4x),装包约 2x,容器启动本身没变快;手机越快,UML 相对 QEMU 的差距越小;有 AVF 就继续用 AVF,它比另外两个快大约十倍。

官方文档另给的经验值也值得记住:QEMU TCG 对 CPU 密集任务比原生慢 5–20 倍,JIT 型负载(Node、JVM、编译器)因为双重 JIT 更惨;而在 arm64 客户机里拉 x86 镜像会套两层模拟,慢 50–100 倍——所以镜像一定要选 linux/arm64。

四、五分钟上手

1. 装 APK。 从 Releases 下 Podroid-v1.3.0-release.apk,在系统设置里给浏览器或文件管理器放开”安装未知应用”,然后打开安装。

2. 走完四步向导。

步骤要选什么注意
持久化存储2 GB – 512 GB,受手机剩余空间限制以后只能扩大,永远不能缩小,宁可选大点
虚拟机配置vCPU 核数、RAM(默认 512 MB)、是否开 SSH打算跑桌面就把 RAM 提到 1 GB 以上
Downloads 共享可选,让安卓和虚拟机互传文件QEMU 后端在部分设备上不稳,出问题先关它
权限通知、无限制后台使用都能跳过,缺了会在首页顶部提示补授

3. 点 Start VM,等 Ready!。 首次启动约 50–60 秒,明显慢于之后的 10–45 秒,因为要做两件一次性的事:Dropbear 生成 RSA/ECDSA/ED25519 三套主机密钥(典型手机上 25–35 秒),以及按你选的大小格式化 ext4 镜像。通知栏会实时显示启动阶段。

4. 打开终端,跑第一条容器命令。

# rootless Podman,无需守护进程,立刻可用
podman run --rm alpine echo "hello from a container"

# Docker 服务已开机自启
docker run -d -p 8080:80 nginx

5. 把服务暴露到手机和局域网。 两条路等价:Settings → Port forwards → Add,或者直接在客户机 shell 里:

podroid-forward add 8080 8080 tcp   # 手机 8080 -> 客户机 8080
podroid-forward list
podroid-forward remove 8080 tcp
podroid-forward clean               # 一次清掉所有用户规则

规则实时生效(QEMU 上走 QMP,不用重启虚拟机)、跨重启保留,也会出现在设置列表里。三个限制:宿主端口必须大于 1024(非特权安卓应用绑不了低端口);尽量避开 32768 以上,安卓给普通应用分配端口从这开始,最容易撞;最多 2048 条规则。

6. 从笔记本 SSH 进去。 向导或设置里开了 SSH 后,同一 Wi-Fi 下:

ssh root@<phone-ip> -p 9922    # 密码 podroid

默认密码务必改掉,或者建个普通用户:adduser -G wheel <username>,wheel 组成员可以用 doas(装了 sudo 也行)。

一个完整的例子,从容器一路验证到手机浏览器:

mkdir -p ~/site && echo '<h1>Hello from Podroid</h1>' > ~/site/index.html

podman run -d --name hello \
  -p 8080:80 \
  -v ~/site:/usr/share/nginx/html:ro \
  docker.io/library/nginx:alpine

wget -qO- http://localhost:8080      # 先在客户机内部确认
podroid-forward add 8080 8080 tcp    # 再暴露到手机

然后访问 http://<phone-ip>:8080。手机 IP 可以在 Status 页看,或在客户机里执行 podroid-phone-ip。

顺带一提,整套文档也打包在客户机里,podroid-forward add 9980 9980 tcp 后执行 podroid-docs,就能在离线状态下用浏览器读 http://127.0.0.1:9980。

五、能拿它做什么

自托管小服务。 这是最直接的用途,注意挑 arm64 镜像:

# Gitea:自建 Git
podman run -d --name gitea -p 3000:3000 -p 2222:22 \
  -v gitea-data:/data docker.io/gitea/gitea:latest-linux-arm64

# code-server:浏览器里的 VS Code
podman run -d --name code-server -p 8443:8443 \
  -v "$PWD:/home/coder/project" \
  ghcr.io/coder/code-server:latest \
  --bind-addr 0.0.0.0:8443 /home/coder/project

Pi-hole、Vaultwarden 同理。JVM 系服务(Jenkins、Gradle、Gitea runner)在 QEMU 上会很难受,光 JVM 启动就要 10–30 秒。

随身 Linux 开发机。 工具链装一次就永久留在 overlay 上:

apk add git nodejs npm python3 py3-pip go gcc musl-dev make

SSH 通了之后,笔记本上的 VS Code Remote-SSH 和 JetBrains Gateway 直接连,手机对它们来说就是一台普通远程 Linux 服务器。文本编辑、shell 脚本、Git 操作、短 Python 脚本在 TCG 上都算舒服——这些活主要耗在 IO 和内存上,模拟开销不明显。

学 Linux 和容器。 真内核、真 cgroup v2、真 network namespace、真 nftables,生产服务器上有的特性这里都能摸:

ls /sys/fs/cgroup/
podman run --rm -it alpine sh -c 'ls -la /proc/1/ns/'
nft list ruleset

而且搞坏了不心疼:恢复出厂一分钟内就回到干净的 Alpine。故意把内核搞崩、把磁盘填满、把网络配错,都能立刻重来。

GUI 桌面。 内置 X11 查看器连客户机的 Xvnc,支持触控、外接键鼠和音频转发:

apk add xfce4 xfce4-terminal mousepad ristretto xfce4-taskmanager
apk add firefox gimp

只支持 X11 会话,Wayland 桌面(GNOME Wayland、Plasma Wayland、sway、Hyprland)不渲染。

定时任务和小 bot。 rc-update add crond default && rc-service crond start 之后正常用 crontab,长任务结束时用 podroid-notify "build finished" 直接在安卓通知栏弹一条,点开就回到 Podroid。

六、已知限制与排错

现象原因处理
切到后台虚拟机就死,日志 QEMU crashed (SIGKILL)、exit 137Android 12+ 的 phantom-process killer见下方三条 adb 命令;Android 14+ 可开开发者选项里的 Disable child process restrictions
开机后不久崩溃,提示 Downloads sharing 不稳QEMU 的 virtio-9p 在部分 ARMv9.2 设备(含 Pixel 10)上会崩设置里关掉 Downloads 共享,AVF 路径不受影响
容器慢到离谱拉了 linux/amd64 镜像,套两层模拟换 linux/arm64 镜像
桌面装了但查看器一片空白Wayland 会话不向 X display 渲染用 XFCE/LXDE,或把 GNOME 强制到 Xorg 会话
改了 RAM 或 CPU 核数没变化这两个是启动参数,不能热改停止虚拟机再启动;端口转发和 SSH 开关则即时生效
磁盘不够用overlay 镜像只能扩不能缩设置里扩大,下次启动客户机自动 resize 文件系统
ping 8.8.8.8 通但域名解析失败某些 Wi-Fi 下的已知问题(#93)暂用移动数据
USB Wi-Fi 网卡不工作客户机里没有对应固件apk add linux-firmware-rtlwifi 或对应芯片组的固件包
AVF 多核启动时重启Tensor G3/G4 的 Pixel 8/8a/9 系列副核 bringup 问题v1.2.5 起自动降核重试并记住可用核数,无需手动干预
外接鼠标右键退出全屏安卓全屏手势拦截了右键用双指触摸代替右键
X11 画面模糊或卡顿VNC 客户端只用 Raw 编码,ZRLE 当前关闭查看器设置里选 720p / 900p 预设
首次启用 UML 就失败兼容性测试含 DNS 查询,离线必失败联网后在设置里点 Try UML again
虚拟机卡在 Startingsocket 10 秒超时(多为上次崩溃留下的陈旧 socket)或 60 秒引导超时强制停止 App 再开;导出诊断日志看 console.log 末尾

phantom-process 那三条命令值得单独贴出来,一次执行、跨重启保留:

adb shell device_config set_sync_disabled_for_tests persistent
adb shell device_config put activity_manager max_phantom_processes 2147483647
adb shell settings put global settings_enable_monitor_phantom_procs false

第一条让后续 OTA 推送的标志不会把你的值改回去,后两条把上限抬到 Integer.MAX_VALUE 并关掉执行它的监控器。

排查任何问题的统一入口是 Settings → Export Diagnostic Log,作者收 bug 报告时要的就是这个文件,附上机型和安卓版本。

七、值不值得装

适合这些人:想在手机上跑真容器和自托管小服务;需要一台随身的 Linux 开发机,让笔记本 Remote-SSH 连过来;想认真学 cgroup、namespace、nftables 而不怕搞坏环境;手上有 Pixel 8 及以后的设备,能吃到接近原生的 AVF。

不适合这些场景:32 位 ARM 或 x86 设备、Android 7 及以下(装不上);想在手机上编译 LLVM、内核或大型 Rust 项目(TCG 下是小时级,不如在电脑上交叉编译再 scp 过来);把它当生产服务器(后台随时可能被系统杀,存储和网络都有硬限制);需要跑不可信代码(尤其 UML 后端根本不是安全边界)。

和 Termux 是互补,不是替代。 Termux 轻、秒开、适合日常命令行;Podroid 重(300 MB+ 的 APK 加几 GB 磁盘),换来的是完整内核和真容器。两个都装着,按活儿选,是最舒服的用法。

最后三条安全提醒:默认的 root / podroid 一定要改;SSH 和端口转发是暴露在整个局域网的,公共 Wi-Fi 下别开;UML 后端的客户机内核以应用自身权限运行,隔离性远不如 QEMU/AVF。


参考

← 返回博客