前两天有读者问我,手边只有一台安卓手机,能不能临时把 Java Web 项目跑起来给老师或者客户看一眼。我说可以,而且不是非得插电脑。做法就是老掉牙但稳定的那套:在 Termux 里装一个 Ubuntu 用户区,再在 Ubuntu 里手动装 Tomcat。这个方案我反复用过好几次,坑基本都踩平了。这篇就把它从零到一的完整过程写出来,同时解释清楚每一步背后的“为什么”。适合想在 Android 上跑 Tomcat、想把旧手机改造成内网小服务器的朋友。如果你已经在 PC 上装过 Tomcat,重点看第 4、5 章,能少走不少弯路。
1. 为什么要在 Termux 里套一层 Ubuntu 再装 Tomcat
1.1 Termux 原生环境看起来什么都能装,但总觉得不对劲
Termux 是 Android 上的一个终端模拟器,自带 pkg 包管理工具,能装 Python、nginx、git 等一大堆东西,甚至有人拿它跑 VS Code Server。那为什么不直接在 Termux 里装 Tomcat?问题出在两个方面。
第一,Termux 的软件包列表里很长一段时间都没有成熟的官方 Tomcat 包,就算你从别处找到编译好的二进制,依赖是不是兼容、更新能不能跟上,都是隐患。第二,Termux 本身的用户区是基于 Android 的,动态库、目录结构、信号处理都跟标准 Linux 发行版有细微差异。你搜到的 Tomcat 配置教程,几乎全部假设你在一台标准 Linux 上操作,套到 Termux 原生环境里,经常会出现“按教程做完但行为不一样”的怪事,排查起来非常磨人。
所以我从一开始就推荐走 proot-distro 装一个 Ubuntu。本质上它就是下载一套完整的 Ubuntu 根文件系统,通过 proot 在用户态做系统调用翻译,不需要 root,也不需要解锁 bootloader。装好之后,你面对的就是一个“标准得不能再标准”的 Ubuntu 环境,Java Web 老一套的部署经验全部可以直接复用。
1.2 搞清楚 proot 的边界,后面才不会懵
proot 不是虚拟机,它没有真正的内核,也没有真正的 init 系统。它是靠 ptrace 把系统调用翻译给 Android 内核。这句话的信息量很大:
- 性能上,磁盘 IO 和进程 fork 会比真机慢不少;
- systemd 不可用,所有依赖 systemctl 的教程都会失效;
- Docker 这类需要内核特权的容器方案跑不起来;
- 但你日常的 Java 进程、网络端口、文件读写都没问题。
这台“手机服务器”的定位很明确:适合临时演示、内网开发、学习 Servlet/JSP 部署、给物联网小项目做管理后台。不适合高并发生产,也千万别拿压测工具去打它。把预期放低,反而会觉得它挺能干。
2. 装 Ubuntu 前的准备:设备检查、Termux 初始化和 JDK 选型
2.1 设备和 Termux 版本前置检查
先说设备门槛。Termux 新版要求 Android 7 以上,如果你手里是 Android 5.1 这种老机器,只能用旧版本的 Termux,软件源也已不完整,体验很差。我的建议是直接换一台设备,别在旧版 Termux 上折腾 Tomcat,这是过来人的血泪。
系统没问题的话,先更新 Termux 自带的基础环境和工具:
pkg update && pkg upgrade -y pkg install proot-distro -y然后安装 Ubuntu:
proot-distro install ubuntu proot-distro login ubuntu首次 login 要初始化 rootfs,时间取决于网络和手机性能,耐心等就行。进入 Ubuntu 之后,立刻做一次系统的完整更新:
apt update && apt upgrade -y apt install -y curl wget这里插一句:如果你在 Termux 终端里敲su,会看到类似~ $ su -m no su program found on this device. termux does not su的提示。这不是报错,而是 Termux 在告诉你它没有系统级 root,除非你另外 root 过手机。proot-distro 是纯用户态模拟 root,完全不依赖 su,所以这个提示可以无视。
2.2 JDK 怎么选:先定 Tomcat 版本,再定 Java 版本
版本选错是新手最常犯的问题。Tomcat 和 Java 的版本有严格的对应关系,装之前先想清楚你要跑什么项目。
| Tomcat 主版本 | 最低 Java 版本 | 实际推荐 | 典型场景 |
|---|---|---|---|
| Tomcat 9.x | Java 8 | Java 11 | 老项目,Servlet 4.0 |
| Tomcat 10.1.x | Java 11 | Java 17 | 新项目,Servlet 5.0/6.0 |
| Tomcat 11.x | Java 17 | Java 17/21 | 官方最新主线 |
如果你没有历史包袱,我的建议是 Tomcat 10.1.x + OpenJDK 17,这也是目前最主流的组合。装 JDK 时用 headless 版,不要装带图形库的完整版:
apt install -y openjdk-17-jdk-headless java -version装完确认一下 JDK 的实际路径,后面要写进 Tomcat 的启动脚本里:
ls -d /usr/lib/jvm/*/在大多数 arm64 设备上,你会看到类似/usr/lib/jvm/java-17-openjdk-arm64的目录。记下这个路径,后面要用。
2.3 proot 环境下安装前你要接受的三个现实
第一,没有 systemd。不要搜“systemctl enable tomcat”之类的教程,这条路在 proot 里走不通。第二,文件操作慢。解压 Tomcat、拷贝 war 包这些操作会比真机慢不少,不是卡死,是慢,要有点耐心。第三,网络端口是和宿主机共享的。Ubuntu 里监听某个端口,等价于整台 Android 手机监听这个端口。这意味着 Termux 侧或者其他 App 占用了端口,Ubuntu 里绑定就会失败。这三个现实理解了,后面遇到问题就不会慌。
3. Tomcat 手动安装全程:从下载到首次启动
3.1 下载 Tomcat:版本号这事千万别偷懒
先到 Apache 官方下载页面看一眼当前稳定版本目录。我写这篇的时候,10.1.x 是稳定线,9.0.x 还在维护。你要是跑老项目就选 9,否则选 10.1。
下载时可以直接用官方目录,但为了速度快,我更习惯从清华 TUNA 镜像站下载,它的 Apache 镜像目录跟官方同步得很及时:
cd /tmp wget https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-10/v10.1.x/bin/apache-tomcat-10.1.x.tar.gz注意:我上面把版本号写成了 10.1.x,实际执行时要去镜像目录里看具体的完整文件名,比如apache-tomcat-10.1.xx.tar.gz,然后替换 x。因为新版本会不断发布,任何固定的版本号都会过时。下载完最好顺手看一眼包大小是否正常,源码包和二进制包几十个 MB 的差异,如果明显小于预期,大概率是下到了残缺文件。
3.2 解压、目录规划与运行时用户
Tomcat 官方习惯把安装目录放在/opt/tomcat,我也沿用这个习惯:
mkdir -p /opt/tomcat tar -xzf apache-tomcat-10.1.x.tar.gz -C /opt/tomcat ln -s /opt/tomcat/apache-tomcat-10.1.x /opt/tomcat/current这里做符号链接current,是为了以后升级版本的时候不用改一堆脚本。接下来创建启动 Tomcat 的专有用户:
useradd -r -s /bin/false tomcat chown -R tomcat:tomcat /opt/tomcat为什么要单独建一个 tomcat 用户?Tomcat 官方文档一直强调不要用 root 运行 Web 容器。一旦 JSP 或者你的应用有漏洞,攻击者拿到的是容器进程的权限。proot 里 root 虽然是模拟的,但养成非 root 运行的习惯,将来部署到真服务器时才不会踩坑。有些教程图省事直接用 root 跑 startup.sh,在测试环境可能没问题,但我劝你别把这个习惯带进生产。
3.3 把 JAVA_HOME 写进 setenv.sh,比写进 .bashrc 靠谱得多
这是整套安装里最容易被忽略、也最容易出问题的一步。
网上很多教程教你export JAVA_HOME=/usr/lib/jvm/...然后写进~/.bashrc。这在交互式终端里没问题,但一旦你的 Tomcat 是通过计划任务、开机脚本或者 Termux:Boot 拉起来的,.bashrc根本不会被执行,结果就是启动脚本找不到 Java,报各种匪夷所思的错误。
正确的做法是把环境变量写进 Tomcat 自己的环境文件。创建$CATALINA_HOME/bin/setenv.sh:
#!/bin/sh JAVA_HOME=/usr/lib/jvm/java-17-openjdk-arm64 CATALINA_HOME=/opt/tomcat/current CATALINA_OPTS="-Xms128m -Xmx512m -Djava.awt.headless=true"然后赋执行权限:
chmod +x /opt/tomcat/current/bin/setenv.shsetenv.sh 会在 catalina.sh 启动时被自动读取,不需要你手动 source。这里面写死的JAVA_HOME路径必须和之前ls /usr/lib/jvm看到的一致,架构是 arm64 还是 armhf,决定了路径最后一段是java-17-openjdk-arm64还是java-17-openjdk-armhf。
3.4 配置管理账号并完成首次启动
Tomcat 自带的 manager 应用默认不允许任何用户访问,需要手动在conf/tomcat-users.xml里加账号。在</tomcat-users>标签前添加:
<role rolename="manager-gui"/> <role rolename="admin-gui"/> <user username="admin" password="change-me" roles="manager-gui,admin-gui"/>这个密码我只是写个示例,强烈建议你改成足够复杂的强密码。Tomcat 的 manager 页面一旦暴露,弱口令等于把服务器拱手送人。
然后用 tomcat 用户启动:
su -s /bin/bash tomcat -c "/opt/tomcat/current/bin/startup.sh"启动后立刻验证:
curl -I http://127.0.0.1:8080如果看到HTTP/1.1 200,说明 Tomcat 已经起来了。浏览器访问http://localhost:8080应该能看到默认首页。如果你想知道 Tomcat 到底有没有完整起来,直接看日志更实在:
tail -f /opt/tomcat/current/logs/catalina.out调试阶段我更喜欢用前台模式启动,任何错误会直接打在终端上,不用反复翻日志文件:
su -s /bin/bash tomcat -c "/opt/tomcat/current/bin/catalina.sh run"4. 实测踩坑:端口占用、JAVA_HOME 和 systemd 缺失的排查链路
4.1 startup.sh 执行了,8080 却没反应,一步步查
这个症状出现过很多次。启动脚本没有报错,但 curl 就是连不上。我会按照下面的链路排查:
- 先看
logs/catalina.out有没有异常,很多问题在这里已经现出原形; - 再看
logs/localhost.202x-xx-xx.log,这个文件记录了 Web 应用的部署过程; - 如果日志为空,那就是 Tomcat 进程根本就没起来,十有八九是 JAVA_HOME 没传给 JVM。
最后一个问题的根源几乎都是 setenv.sh 没写好,或者写对了但没加执行权限。我遇到过一种比较隐蔽的情况:在交互终端里手动敲 export 之后能启动,但退出重开终端再启动就失败。原因就是 Tomcat 启动脚本读的是 setenv.sh,而不是你终端里的环境变量。只要把JAVA_HOME写死在 setenv.sh 里,这个问题就根治了。
4.2 BindException:8080 端口被 Termux 侧或别的 App 占了
proot 里 Ubuntu 和 Termux 共享同一个网络栈,所以 Termux 原生环境里如果运行了某个服务绑了 8080,你 Ubuntu 里的 Tomcat 再绑定就会失败。启动日志里会出现BindException: Address already in use这样的信息。
排查方式是先看清端口到底被谁占了。在 Ubuntu 里执行:
apt install -y iproute2 ss -ltnp | grep 8080如果看到宿主机层面的进程占着端口,那就要么停掉那个服务,要么给 Tomcat 换端口。换端口很简单,编辑conf/server.xml,找到 HTTP Connector 这一段:
<Connector port="8080" protocol="HTTP/1.1"... />把8080改成8081,重启 Tomcat。记住 Tomcat 连接器的端口、protocol、redirectPort 这些属性是配套的,只改 port 一般就行,但如果后面要配 HTTPS,别忘了一起调整其他属性。
另外,因为 proot 下文件系统是模拟的,有时候ss的信息不一定准确,我会再用一个笨办法确认:直接启动一个临时监听线程试试绑定。在 Termux 侧执行同样的端口检查,也能帮你判断是不是宿主侧的问题。
4.3 网上教程全是 systemctl,在 proot 里全部失效
当你搜索“Tomcat 开机自启”,看到的内容基本都长这样:systemctl enable tomcat、systemctl start tomcat。在 proot 的 Ubuntu 里执行这些命令,只会得到System has not been booted with systemd之类的提示。
这不是你操作错了,而是 proot 里根本没有 systemd 这个 init 系统。因为 proot 没有真实的内核初始化能力,PID 1 只是你 login 进来的 shell,systemd 无法作为系统第一个进程启动。
所以前面的内容里我特意强调要手工维护startup.sh。如果想开机自启,得借助 Termux 侧的方案,这个我放在第 5 章展开。核心逻辑就是:不要试图在 Ubuntu 内部解决自启问题,要把自启这件事放到 Termux 侧去触发,因为 Termux 才是真正被 Android 系统“养着”的那个进程。
4.4 锁屏十分钟,Tomcat 就没了
这个坑很隐蔽。Tomcat 本来跑得好好的,锁屏放口袋里,过一会儿再拿出来访问,发现连不上了。打开 Termux 一看,整个 Ubuntu 会话都退出了。
原因是 Android 的省电策略。系统认为 Termux 是后台应用,为了省电直接杀掉了它的进程,于是 proot 里的 Ubuntu 和 Tomcat 一起陪葬。解决方案是双管齐下:
- 在系统设置里把 Termux 加入电池优化白名单,允许它在后台保持运行;
- 在 Termux 终端里执行
termux-wake-lock,这个命令会请求一个唤醒锁,让 CPU 保持工作状态。
另外,给 JVM 分配的内存也不要贪大。手机上 JVM heap 占太多,系统会判定内存压力大,杀后台更果断。512M 以内是安全线,也就是我前面 setenv.sh 里写-Xmx512m的原因。
方便查看,把这一章的排查场景整理成表:
| 症状 | 最可能根因 | 处理方式 |
|---|---|---|
| startup.sh 执行后 8080 没动静 | JAVA_HOME 未传入 | setenv.sh 写死路径 |
| BindException 启动失败 | 端口被 Termux 或其他 App 占用 | 换端口或停服务 |
| systemctl 命令报错 | proot 里没有 systemd | 改用 startup.sh + Termux 侧自启 |
| 锁屏后进程消失 | Android 省电策略杀进程 | 电池优化白名单 + termux-wake-lock |
5. 把它变成一台能长期用的小服务器:自启、守护与局域网访问
5.1 用 Termux:Boot 实现开机后自动拉起整套环境
Termux 有个官方插件叫 Termux:Boot,专门用来在设备开机时执行指定脚本。它会在开机后运行~/.termux/boot/目录下的所有脚本。我在这里放一个start-tomcat.sh,内容很简短:
#!/data/data/com.termux/files/usr/bin/bash termux-wake-lock proot-distro login ubuntu -- /opt/tomcat/current/bin/startup.sh写完赋执行权限:
chmod +x ~/.termux/boot/start-tomcat.sh这条命令的逻辑是把 Tomcat 的启动脚本作为启动参数传给 proot-distro,也就是“先登录 Ubuntu,然后执行一次 startup.sh”。因为 setenv.sh 已经把 JAVA_HOME 写死了,所以在非交互环境下启动也不会出现找不到 Java 的问题。这一步就是前面花那么多功夫写 setenv.sh 的真正回报。
5.2 加一个简单的进程守护,防止程序崩溃后无人拉起来
Tomcat 在手机这种不稳定的环境里,偶尔会出现进程崩溃或者僵死的情况。我不会上特别复杂的监控系统,一个简单的守护脚本就够了,依然放在 Termux 侧:
#!/data/data/com.termux/files/usr/bin/bash while true; do if ! curl -sf http://127.0.0.1:8080 >/dev/null; then proot-distro login ubuntu -- /opt/tomcat/current/bin/startup.sh fi sleep 30 done用 nohup 在 Termux 侧挂起来:
nohup ~/.termux/boot/guard-tomcat.sh >/tmp/tomcat-guard.log 2>&1 &这里有一个经验:不要把守护脚本跑在 Ubuntu proot 内部再用 nohup,因为 proot 会话退出时整个进程树会被清理。挂在 Termux 宿主侧,守护进程的父进程是 Termux,不会跟着 Ubuntu 会话一起没。
5.3 局域网访问:手机 IP 才是你的服务器地址
Tomcat 起来之后,真正要访问的是你那台安卓手机在局域网里的 IP。在 Termux 里查看:
ip -4 addr show wlan0 | grep inet假设得到的 IP 是192.168.1.100,那同一 WiFi 下的电脑浏览器访问http://192.168.1.100:8080就能看到 Tomcat 首页。如果连不上,我的排查顺序是:
- 手机和电脑是否在同一网段;
- 路由器或热点是否开启了 AP 隔离,有些公共热点会禁止客户端互相访问;
- 防火墙、安全软件有没有拦截 8080 入站连接。
安全上我多说一句,Tomcat 的 manager 账号千万别用 admin/123456 这种弱口令,也更不要把 8080 端口直接暴露到公网。如果你确实需要外部访问,那涉及的内容比这里要深得多,建议只在可信网络里使用,先把内网这台小服务器玩明白再说。
5.4 性能预期:这台“手机服务器”到底能扛多少
proot 本身有系统调用翻译的开销,加上手机 SoC 的散热和功耗限制,性能预期要现实一点。我实测下来,一个简单的 CRUD 接口,十几二十个并发没问题,日常给自己或者小团队做个演示,响应速度完全能接受,但没必要的压测就省了。
存储方面,Ubuntu rootfs 加 JDK 加 Tomcat 加一个应用,预留 2GB 空间比较稳妥。内存给 JVM 的堆控制在 512M 以内,手机本身还要跑 Android 系统,内存榨干只会导致系统频繁杀进程,得不偿失。JVM 参数里加上-Djava.awt.headless=true也是为了避免在无头环境下操作图形相关组件时报错。
如果你在 PC 上装过 Tomcat,会发现这套流程的核心逻辑完全一致,变的只是环境边界。遇到的所有问题,总结到一句话就是:“这个环境里什么跟标准 Linux 不一样,我就绕开它”。我的个人习惯是每装完一次,就把 setenv.sh 的 JAVA_HOME、启动命令、访问地址三样记在一个本地笔记里,下次换手机重装能省一半时间。最后再分享一个小技巧:先别急着部署业务应用,把默认的 ROOT 目录换成一个带时间戳的极简 JSP 页面,改完 server.xml 后访问一下,能同时验证端口修改、会话服务和日志输出是否都正常,比盯着配置文件盲改高效得多。