基于 Termux + Ubuntu,在手机上搭建博客与对象存储,并通过 frp / Ngrok 暴露到公网的完整实践笔记。

📌 关于 MinIO 公网暴露方案

本文第 2 节记录的 MinIO 暴露方式是早期方案(frp 隧道 + Cloudflare 灰云直连),
其中 9000 / 9001 端口分离带来的控制台资源加载问题并未彻底解决。

后续已改用 Cloudflare Tunnel(cloudflared) 方案,统一走 Cloudflare Edge,
并用双子域名 + ingress 分别路由 9000 / 9001。最新且推荐的方案请见
《家庭服务器全栈部署完全指南》第 6.5 节。

本文的 frp 方案仍可作为「国内直连优先」场景的参考,但请优先阅读上述新方案。


1. 私有云架构总览

整套服务运行在 Android 手机的 Termux + Ubuntu 环境中,通过 frp 内网穿透将本地服务暴露到公网,并借助 Cloudflare 进行 DNS 解析与代理。

公网用户
│
├─→ https://blog.nesthub.dpdns.org ──→ Cloudflare(橙云,HTTPS) ──→ frp隧道(type=http) ──→ Nginx:8080 ──→ /var/www/blog
│
└─→ http://minio.nesthub.dpdns.org ──→ Cloudflare(灰云,直连) ──→ frp隧道(type=http) ──→ MinIO:9000

架构要点说明:

组件 作用 运行环境
Termux Android 上的终端模拟器 手机原生
Ubuntu 完整 Linux 环境 Termux 内 proot 容器
Nginx 博客 Web 服务器 Ubuntu 内
MinIO S3 兼容对象存储 Ubuntu 内
frp 内网穿透,将本地端口映射到公网 Ubuntu 内(frpc 客户端)
Cloudflare DNS 解析 + HTTPS 代理 云端

2. 各组件配置清单

2.1 内网穿透(frp)

frp 客户端(frpc)运行在 Ubuntu 内,通过两条隧道分别将博客和 MinIO 服务暴露到公网。

隧道 配置文件 类型 本地端口 公网地址
博客 nginx.ini http 8080 https://blog.nesthub.dpdns.org
MinIO minio.ini http 9000 http://minio.nesthub.dpdns.org

配置示例(frpc.ini 风格):

# nginx.ini
[common]
server_addr = <frp服务器地址>
server_port = <frp服务器端口>

[nginx]
type = http
local_port = 8080
custom_domains = blog.nesthub.dpdns.org
# minio.ini
[common]
server_addr = <frp服务器地址>
server_port = <frp服务器端口>

[minio]
type = http
local_port = 9000
custom_domains = minio.nesthub.dpdns.org

2.2 Nginx(博客服务)

Nginx 作为博客的 Web 服务器,监听 8080 端口,对外提供静态网站服务。

配置项 值
配置文件路径 /etc/nginx/sites-available/blog
监听端口 8080
域名 blog.nesthub.dpdns.org
网站根目录 /var/www/blog
运行状态 ✅ 运行中

Nginx 配置要点:

server {
listen 8080;
server_name blog.nesthub.dpdns.org;

root /var/www/blog;
index index.html index.htm;

location / {
try_files $uri $uri/ =404;
}
}

提示:Nginx 监听 8080 而非 80,是为了避免与 Termux 环境中可能存在的端口冲突,同时 frp 隧道也指向 8080。

2.3 MinIO(对象存储)

MinIO 是一个 S3 兼容的高性能对象存储服务,用于存储图片等静态资源。

配置项 值
启动命令 minio server /data/minio --console-address :9001 --address :9000
API 端口 9000
控制台端口 9001
Access Key <用户名>
Secret Key <密码>
数据目录 /data/minio
运行状态 ✅ 运行中

端口说明:

  • 9000 端口:S3 API 接口,用于程序化访问(PicGo 上传、mc 命令等)
  • 9001 端口:Web 管理控制台,用于浏览器可视化操作

安全提示:这里的凭据已脱敏为占位符。实际使用时请设置强密码,并通过环境变量 MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD 传入,避免写死在命令或配置文件里。

2.4 Cloudflare 设置

Cloudflare 负责域名解析,对博客启用代理(橙云)以获得 HTTPS,对 MinIO 使用仅 DNS 模式(灰云)以保持直连速度。

子域名 记录类型 目标 云朵状态 SSL/TLS 模式
blog CNAME uk.frp.one 🟠 橙色(代理) Flexible(灵活)
minio CNAME uk.frp.one ⚪ 灰色(仅 DNS) —

两种模式的区别:

  • 橙云(代理模式):流量经过 Cloudflare 代理,自动获得 HTTPS、DDoS 防护、CDN 加速。适合博客等面向用户的 Web 服务。
  • 灰云(仅 DNS):Cloudflare 仅做 DNS 解析,流量直连服务器。适合需要低延迟的 API 服务(如 MinIO),避免 Cloudflare 海外节点增加延迟。

2.5 SSL 证书

配置项 值
证书路径 /etc/nginx/ssl/nesthub.crt
私钥路径 /etc/nginx/ssl/nesthub.key
签发工具 acme.sh
签发方式 手动 DNS 验证(Let’s Encrypt)
有效期 90 天
续期方式 手动执行续期命令

续期命令:

./acme.sh --renew -d nesthub.dpdns.org --dns dns_manual --server letsencrypt --force
nginx -s reload

重要:由于使用手动 DNS 验证,无法自动续期。请在证书到期前手动执行上述命令,并在域名 DNS 中添加对应的 TXT 记录。

2.6 mc 命令行工具

mc 是 MinIO 客户端工具,用于在命令行中操作 MinIO 对象存储。

设置别名:

mc alias set myminio http://minio.nesthub.dpdns.org <用户名> <密码>

设置别名后,即可通过 mc 命令方便地管理存储桶和文件,无需每次输入完整的连接信息。


3. 关键命令速查表

服务管理

操作 命令
启动 Nginx nginx
重载 Nginx 配置 nginx -s reload
测试 Nginx 配置 nginx -t
启动 MinIO(后台) nohup minio server /data/minio --console-address :9001 --address :9000 > /tmp/minio.log 2>&1 &
启动 frpc - 博客隧道 cd /root/frp && nohup ./frpc -c nginx.ini >> /var/log/frpc-nginx.log 2>&1 &
启动 frpc - MinIO 隧道 cd /root/frp && nohup ./frpc -c minio.ini >> /var/log/frpc-minio.log 2>&1 &

日志与诊断

操作 命令
查看 frpc 博客日志 tail -20 /var/log/frpc-nginx.log
查看 MinIO 日志 tail -20 /tmp/minio.log
测试博客(本地) curl -I http://127.0.0.1:8080
测试 MinIO(本地) curl -I http://127.0.0.1:9000/minio/health/live

MinIO 存储管理

操作 命令
列出所有存储桶 mc ls myminio
创建存储桶 mc mb myminio/picture
设置存储桶为公开只读 mc anonymous set download myminio/picture
上传文件 mc cp localfile.jpg myminio/picture/
列出存储桶内文件 mc ls myminio/picture

4. 已知局限与注意事项

4.1 Termux 环境限制

Termux 是 Android 上的终端模拟器,并非完整的 Linux 环境,存在以下限制:

  • netstat、ss、lsof 等网络工具功能受限或无法使用
  • 推荐使用 curl -I 测试服务状态,代替端口扫描工具
  • 部分系统级操作需要 proot 模拟,性能有一定损耗
  • 后台进程可能被 Android 系统省电策略杀掉,需注意保活

4.2 SSL 证书续期(重要)

证书使用手动 DNS 验证方式签发,无法自动续期:

  • 有效期为 90 天
  • 到期前需手动执行续期命令(见 2.5 SSL 证书)
  • 续期时需要在 DNS 中添加 TXT 记录进行验证
  • 续期完成后需重载 Nginx 使新证书生效

4.3 MinIO 访问方式

访问方式 地址 说明
Web 控制台 http://minio.nesthub.dpdns.org:9001 需在 frp 中额外转发 9001 端口
API / S3 访问 http://minio.nesthub.dpdns.org 已通过 frp 转发 9000 端口

当前仅转发了 9000 端口,如需通过公网访问 Web 控制台,需在 frp 中新增一条 9001 端口的隧道配置。

4.4 PicHoro 等手机 App 连接 MinIO 的问题

部分手机端 S3 客户端在连接 MinIO 时可能遇到兼容性问题:

  • 部分 App 无法正确处理自定义 Endpoint 或非标准端口
  • 建议优先使用 mc 命令行工具进行管理
  • 或更换为 Simple S3 Browser、S3Drive 等兼容性更好的工具

5. 当前具备的能力

能力 说明
✅ HTTPS 公网博客 通过 https://blog.nesthub.dpdns.org 访问 Hexo 博客
✅ HTTP 公网对象存储 通过 http://minio.nesthub.dpdns.org 访问 MinIO(S3 兼容)
✅ mc 命令行管理 完整的 MinIO 存储桶和文件管理能力
✅ 国内直连高速访问 MinIO 使用灰云直连,速度不受 Cloudflare 海外节点影响
✅ 低功耗低维护 所有服务运行在 Android 手机的 Termux Ubuntu 中

6. 后续优化建议

  1. 设置定时任务自动续期证书

    • 如果改用 Cloudflare API 模式签发,可实现全自动续期
    • 配合 crontab 定期执行 acme.sh 续期命令
  2. 配置 MinIO 的 HTTPS 访问

    • 如需公网加密访问 MinIO,可改用 TCP 隧道 + 本地证书方案
    • 或在 Nginx 层做反向代理,由 Nginx 处理 TLS
  3. 添加更多服务

    • 复用同样的 frp 隧道模式,可扩展 Nextcloud(私有云盘)、Jellyfin(媒体服务器)等
    • 每个服务新增一条 frp 隧道和一条 Cloudflare DNS 记录即可
  4. 使用 tmux 或 screen 管理后台进程

    • 避免 SSH 断开连接时服务停止
    • 推荐 tmux new -s services 创建专用会话
  5. 配置防火墙与访问控制

    • 限制 MinIO 的公开访问权限,仅对特定存储桶开放只读
    • 定期更换 Access Key / Secret Key

7. Ngrok 内网穿透指南

7.1 Ngrok 基础认知

什么是 Ngrok?

Ngrok 是一个内网穿透工具,可以将本地运行的 Web 服务(如 localhost:80)暴露到一个公网可访问的 URL 上,极大地方便了开发和测试。

核心命令解析

以命令 ngrok http --url=<你的域名>.ngrok-free.app 80 为例:

参数 说明
ngrok http 建立 HTTP 隧道,将公网流量转发到本地端口
80 本地服务监听的端口号(如 Nginx、Apache 或本地 Web 项目)
--url=<你的域名>.ngrok-free.app 强制绑定固定的公网子域名

固定子域名的特点:

  • 全球唯一,先到先得
  • 需提前在 ngrok 官网后台(Dashboard → Cloud Edge → Domains)进行认领(Reserve)
  • 未认领时执行命令会报错:This domain is not reserved for your account
  • 免费版认领的域名长期无流量可能被回收

7.2 免费版拦截页面问题

问题现象

访问 ngrok 免费版提供的 URL 时,会看到警告页面,需要手动点击 “Visit Site” 才能继续访问:

You are about to visit: xxx.ngrok-free.app
We display this page to prevent abuse...

产生原因

这是 ngrok 为防止平台被滥用(如网络钓鱼)而设置的安全措施。所有免费版用户首次访问时都会遇到此拦截。

绕过方案对比

方案 操作方式 适用场景 成本
添加请求头 请求时携带 ngrok-skip-browser-warning: true API 调用、后端服务、程序化访问 免费
手动点击 首次访问时在浏览器点击 “Visit Site” 个人临时测试(浏览器保存 7 天免验证 Cookie) 免费
升级付费计划 购买 Pro 及以上套餐 生产环境、客户演示、彻底免打扰 付费

7.3 Typora 图片无法预览问题

问题描述

  • 使用 PicGo 上传图片至 ngrok 搭建的图床
  • Typora 中粘贴图片后显示裂图(无法加载)
  • 浏览器直接打开图片链接会被 ngrok 拦截,显示警告页面

根本原因

  1. Typora 通过 URL 加载图片时,相当于浏览器请求
  2. ngrok 返回拦截页面(HTML)而非图片数据(JPEG/PNG),Typora 无法将 HTML 渲染为图片
  3. Typora 1.6.7 版本的 Bug:conf.user.json 中配置的 customHeaders(用于添加 ngrok-skip-browser-warning)失效

解决方案 A:本地代理中转(推荐,最稳定)

核心思路:在本地运行一个代理服务,转发图片请求至 ngrok,并自动添加”免拦截”请求头。

方式一:使用 Docker(快捷)

docker run -d --name ngrok-proxy -p 8080:80 igops/ngrok-skip-browser-warning

方式二:使用 Python Flask(无需 Docker)

from flask import Flask, request, Response
import requests

app = Flask(__name__)
TARGET = "https://<你的域名>.ngrok-free.app"

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def proxy(path):
url = f"{TARGET}/{path}"
resp = requests.get(url, headers={"ngrok-skip-browser-warning": "true"}, stream=True)
return Response(resp.iter_content(chunk_size=1024),
content_type=resp.headers.get('content-type'),
status=resp.status_code)

if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080)

PicGo 配置修改:

将图床设置中的**”自定义URL”或“URL前缀”**修改为 http://127.0.0.1:8080。

⚠️ 修改后需重新上传图片,Typora 中的链接才会替换为新地址。

解决方案 B:Cloudflare Workers 云端代理(不依赖本地)

核心思路:在 Cloudflare 边缘网络部署 Worker,无需在本地运行任何程序。

完整 Worker 脚本:

const TARGET_URL = 'https://<你的域名>.ngrok-free.app';

export default {
async fetch(request) {
const url = new URL(request.url);
const path = url.pathname + url.search;
const target = TARGET_URL + path;

const modifiedRequest = new Request(target, {
method: request.method,
headers: request.headers,
body: request.body,
});

// 关键:添加绕过拦截的请求头
modifiedRequest.headers.set('ngrok-skip-browser-warning', 'true');

const response = await fetch(modifiedRequest);
return response;
},
};

部署步骤:

  1. 登录 Cloudflare Dashboard → Workers & Pages
  2. 创建新 Worker,粘贴上述脚本
  3. 部署后获得 xxx.workers.dev 域名
  4. 修改 PicGo 的 URL 前缀为 https://你的Worker域名.workers.dev

优点:

  • 每天 10 万次免费请求,个人使用绰绰有余
  • 无需本地运行任何程序,全云端代理
  • 不受本地网络环境影响

7.4 Linux 服务器自动化运维

编写启动脚本

保存路径 /root/start-ngrok.sh:

#!/bin/bash
# 后台启动 ngrok,日志输出到 /tmp/ngrok.log
nohup ngrok http --url=<你的域名>.ngrok-free.app 9000 > /tmp/ngrok.log 2>&1 &

# 打印进程 PID 以便管理
echo "ngrok started in background, PID: $!"

赋予执行权限:

chmod +x /root/start-ngrok.sh

一键创建脚本(使用 heredoc)

sudo cat > /root/start-ngrok.sh << 'EOF'
#!/bin/bash
nohup ngrok http --url=<你的域名>.ngrok-free.app 9000 > /tmp/ngrok.log 2>&1 &
echo "ngrok started in background, PID: $!"
EOF
sudo chmod +x /root/start-ngrok.sh

配置 .bashrc 实现登录自启

需求:每次登录时检查 ngrok 是否运行,若未运行则启动,防止重复执行。

在 /root/.bashrc 末尾添加:

# 自动启动 ngrok(仅当尚未运行时)
if ! pgrep -f "ngrok http --url=<你的域名>.ngrok-free.app" > /dev/null; then
/root/start-ngrok.sh
fi

命令解析:

  • pgrep -f:根据完整的命令行字符串匹配进程
  • 若匹配不到进程,则执行启动脚本
  • 该检查机制确保了无论是第一次登录还是多次 source,ngrok 都只会运行一份实例

7.5 排错与最佳实践

常见错误排查

错误现象 可能原因 解决方案
command not found: ngrok ngrok 未安装或未加入 PATH 安装 ngrok 并执行 export PATH=$PATH:/path/to/ngrok
This domain is not reserved 未认领该固定域名 登录 ngrok 官网,在 Cloud Edge → Domains 中认领
Typora 仍显示裂图 PicGo 自定义 URL 未修改 检查 PicGo 设置,将域名改为代理地址;重新上传图片
ngrok 后台进程被杀 关闭终端导致进程终止 必须使用 nohup 启动,并配合 & 放到后台

管理命令速查

操作 命令
查看实时日志 tail -f /tmp/ngrok.log
查看进程 ps aux | grep ngrok
停止服务 pkill ngrok 或 kill <PID>

推荐生产环境替代品

如果长期依赖内网穿透且需要稳定、无拦截服务,可考虑:

方案 优点 缺点
Cloudflare Tunnel(cloudflared) 官方免费、无拦截页面、自带 HTTPS 需要域名托管在 Cloudflare
frp 开源免费、完全自控、支持 TCP/UDP 需要有公网服务器作为中转
Ngrok 付费版 支持自定义域名、并发限制更高、无警告页 需要付费

8. 方案对比与选型总结

维度 frp 方案(私有云) Ngrok 方案(图床代理)
适用场景 长期稳定运行的服务 临时开发测试、图床中转
域名 自有域名 + Cloudflare DNS ngrok 提供的子域名
HTTPS Cloudflare 代理 / acme.sh 证书 免费版有拦截页
稳定性 高(自建服务器) 中(依赖 ngrok 服务)
成本 0(已有 frp 服务器) 0(免费版)/ 付费版
推荐指数 ⭐⭐⭐⭐⭐ ⭐⭐⭐(临时使用)

总结:对于长期运行的私有云服务(博客、对象存储),推荐使用 frp + Cloudflare 方案;对于临时开发测试或图床代理,可使用 Ngrok 配合代理绕过方案。两者也可组合使用,互为补充。