自托管服务只在局域网能用?用反向代理收敛统一入口,cpolar 留一份只读访问清单
自托管玩到第三个月,我就被一堆端口号搞烦了。Jellyfin 在8096,Home Assistant 在8123,还有一个自建的记事本在5230,家里三台设备上的浏览器书签加起来记了七八条192.168.x.x:xxxx。换台机器、换个网络,就得重新翻一遍。
真正让我下决心整治的,是上个月同事问"你那套东西能让我看一眼吗"。我没法直接甩一堆内网地址过去——他不在我家局域网里,那些地址对他就是死链。
这篇文章要解决的就是这个:先用反向代理把多个本地服务收敛到一个统一入口,再把这份"入口清单"做成只读页面,最后用 cpolar 把只读清单开给异地同事。对方不需要连进我的内网,也不需要知道我有几个服务在跑。
先划一下边界,省得你对这篇文章的期待跑偏:本文是一份「隔离环境中的可执行实验提纲」,所有配置片段都是骨架,不是我在生产机上跑出来的实测快照。我这一轮没有实际安装、没有启动服务,所以文中不会出现任何吞吐、延迟、并发这类需要真机跑出来的数值。你要做的是照骨架在你自己的环境里补齐、跑通、再按需调整。
1 为什么要收敛入口,而不是继续加书签
先把问题拆开看。多服务自托管的痛点其实有三个层次:
第一层是记不住。服务一多,端口号就是随机数字,大脑记不住,书签越堆越乱。
第二层是地址散。同一个服务,在内网叫192.168.1.20:8096,在外面完全不可达;换台设备还得重新配。
第三层是说不清。想把某一两个服务临时分享出去,要么把整个内网暴露出去,要么一个个端口单独做映射,越做越乱。
反向代理解决的正好是前两层:所有服务挂到同一个入口后面,按域名或者按路径分流。你在浏览器里只记一个入口地址,后面换端口、换机器、加服务,对使用者来说是透明的。
至于第三层,我们不追求把服务本体开放出去,而是单独做一份只读清单页——上面列清楚有哪些服务、各自的入口路径、当前状态,但不提供任何操作按钮。这样即使用 cpolar 开到了公网,最坏情况也只是"被看到清单",不会变成"被改了配置"。
2 环境准备:一台能跑的机器加两个示例服务
概念说完了,动手之前先把场地备好。
2.1 你需要准备什么
- 一台跑着自托管服务的机器,可以是家里的 NAS,也可以是一台常开的小主机
- 至少两个能在本地访问的 HTTP 服务,作为被代理对象
- 一个反向代理软件,本文用配置文件骨架来演示思路,你按自己用的那款代理软件对齐写法
- cpolar,用来把最后的只读清单页开到公网
两个示例服务不一定要多正式。你现有的任意两个 Web 服务都行,只要它们各自监听一个本地端口,本机能打开。这一步先别急着上代理,先确认每个服务在本机单独能访问——代理出问题时,这一步是最快的排除依据。
2.2 先把"本机能访问"钉死
在你配代理之前,逐个确认服务是活的。假设有两个服务分别监听8096和8123,先在本机各自打开一次:
curl -sI http://127.0.0.1:8096 | head -n 1 curl -sI http://127.0.0.1:8123 | head -n 1看到HTTP/1.1 200或者302、401之类的正常响应头,就说明服务本身没问题。这里如果连状态行都出不来,先别往下走,去查这个服务是不是没起来、端口是不是被别的进程占了。
提醒一句:这一步别用服务的内网 IP 去测,就用127.0.0.1。因为我们要验证的是"服务本身活着",而不是"网络通不通",把变量拆开排错会轻松很多。
3 用反向代理把多个服务收敛到一个入口
现在把代理架到前面。核心思路只有一句话:外部只认一个入口,入口按规则把请求转发给后面的某个服务。
3.1 收敛的两种切法:按路径还是按域名
分流规则基本落在这两种形态上:
- 按路径分流:同一个域名或 IP,用
/jellyfin、/ha这样的路径前缀区分服务 - 按域名分流:用
jellyfin.home.lan、ha.home.lan这样的子域名区分服务
按路径的好处是只需要一个域名,内网测试阶段最省事;按域名的好处是每个服务拿到"干净的根路径",不容易因为路径前缀导致前端的静态资源 404。
我自己的习惯是:内网联调阶段先用路径分流,跑通之后再换成域名分流。因为路径分流对 DNS 的要求最低,先把它跑通,能快速确认代理这层是通的。
3.2 一段配置骨架长什么样
下面是一段语法正确的骨架配置,用来演示"一个入口、两个上游"的结构。不同代理软件的字段名不一样,你要按自己用的那款对齐,不要直接照抄字段名。
# 骨架示例:一个入口,两个上游服务 # 字段名以你实际使用的反向代理软件官方文档为准 server { listen 80; server_name entry.home.lan; # 统一入口 # 上游一:自建记事本 location /notes/ { proxy_pass http://127.0.0.1:5230/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上游二:家庭自动化面板 location /ha/ { proxy_pass http://127.0.0.1:8123/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }几个容易踩的点,先记下来:
proxy_pass结尾的斜杠,会决定路径是被"替换"还是被"追加"。/notes/配http://127.0.0.1:5230/是替换型,请求打过去时前缀会被去掉;漏掉一个斜杠就会变成追加型,后端收到的是/notes/xxx,对应路由不存在就是 404。Host头要透传。有些后端会拿Host做校验或者拼跳转地址,不透传就会出现"首页能打开、点一下就跳回内网 IP"的诡异现象。- 改完配置先做语法自检再加载,别直接把生产入口搞挂。多数代理软件都提供配置测试命令,跑通了再重载。
这一步做完应该看到什么:浏览器打开统一入口,/notes/和/ha/分别能出两个不同服务的页面。如果只有一个能开、另一个 502,优先回去看这个上游端口是不是又掉了;如果两个都能打开、但页面样式全乱,优先怀疑路径前缀和静态资源路径没对齐。
4 做一份只读访问清单页
入口收敛完了,但"我到底有哪些服务"这件事还是散在配置文件里。这一步我们把清单单独做成一个页面。
4.1 清单页要放什么
一份够用的访问清单,内容不需要花哨:
- 服务名称
- 对应的入口路径或子域名
- 一句话说明它是干什么的
- 当前状态(在线 / 离线)
4.2 关键约束:这份清单必须是只读的
这是整篇文章最需要强调的一点,清单页不能有任何写入接口。
具体来说:
- 不要给它挂任何"重启服务""修改配置""删除条目"的按钮或 API
- 清单内容由后端只读读取,比如从一个静态 JSON 文件或者只读接口取值
- 页面本身不做任何状态变更动作,只负责展示
为什么这么强调?因为这份清单接下来要经过 cpolar 开到公网。只要它是纯展示、无写入路径,最坏的情况也只是"清单被别人看到",而不会升级成"配置被改动"。能公开的东西,和能改动的东西,必须是两个界面。
4.3 一个最小清单的数据骨架
清单页后端读的可以就是这么一份静态数据,字段简单到不能再简单:
{ "services": [ { "name": "自建记事本", "path": "/notes/", "desc": "随手记和待办", "status": "online" }, { "name": "家庭自动化面板", "path": "/ha/", "desc": "灯光和传感器状态", "status": "online" } ] }页面只读取这个文件的name / path / desc / status四个字段来渲染成一张表。这里刻意不提供任何写接口,因为一旦有了写路径,就等于把"改配置"的能力挂到了公网入口上。
清单页本身也走统一入口,可以挂在/list/这样的路径下,和前面两个服务并列,不需要额外开端口。
5 用 cpolar 把只读清单开给异地同事
内网里的入口和清单都跑通了,现在解决开头那个问题:让不在内网的同事也能看。
5.1 为什么只开清单,不开服务本体
回到最初的场景——同事只是"想看看到底有哪些服务在跑",而不是"想操作我的服务"。需求本身是只读的,那我们就只开只读的那一页。
这样做的好处很直接:
- 同事拿到的地址后面只有一份清单,没有操作面板
- 即使地址被人转发、被人扫到,被看到的最多是一张服务列表
- 我不用为了"让人看一眼"就把整套内网入口暴露出去
5.2 安装 cpolar
cpolar 的官方入口和下载页在:
- 官网:
https://www.cpolar.com/ - 下载页:
https://www.cpolar.com/download - 文档首页:
https://www.cpolar.com/docs/
Linux 环境官方给了一键安装脚本:
curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bashmacOS 用 Homebrew:
brew tap probezy/core && brew install cpolar sudo cpolar service install sudo cpolar service start装完先确认服务起来了:
cpolar version curl -s http://127.0.0.1:9200 || echo "服务未启动"9200是 cpolar 的本地 Web UI 端口,能打开就说明服务在跑。如果这里打不开,先解决 cpolar 服务本身,别急着开隧道——隧道命令是依赖这个服务在后台运行的。
5.3 登录并绑定账号
有两种方式,按你的环境挑:
- 有图形界面:直接打开
http://127.0.0.1:9200登录,很多场景下登录后会自动把邮箱和 authtoken 写进配置文件 - 纯命令行环境:手动绑定,先去后台拿 token
拿 token 的路径是:登录https://dashboard.cpolar.com/login,点顶部导航的验证,进入https://dashboard.cpolar.com/auth,页面标题是「你的隧道 Authtoken」,下面文本框就是 token,旁边有复制按钮。
拿到之后:
cpolar authtoken 你的token5.4 把清单页映射成公网地址
清单页是 HTTP 页面,所以开 HTTP 隧道。假设清单页挂在统一入口的80端口上:
cpolar http 80提醒一句:不要在前台同时开多个命令行隧道,cpolar 官方文档明确提示过,同时开会报"登录实例超过限制"。如果你要长期挂着这个地址,优先走 Web UI 管理,或者配成后台服务。
命令跑起来后,cpolar 会给一个公网地址。先别急着发给同事,自己在浏览器里打开这个地址确认一遍:打开的是不是那份只读清单,而不是统一入口的首页、更不是某个服务的管理面板。这一步是给自己兜底的,确认无误再分享。
5.5 地址会变这件事要提前说
免费套餐的随机公网地址是 24 小时内会变化的。也就是说,如果你把地址发给了同事,第二天大概率就失效了。
如果只是临时给同事看一眼,用随机地址完全够;如果要长期贴在一个文档里让人反复访问,就得考虑固定地址的方案:
- 固定二级子域名需要基础套餐或以上
- 固定 TCP 地址需要专业套餐或以上
- 自定义域名需要专业套餐或以上
这三种能力对应的套餐门槛不同,选之前先想清楚你是"临时看一眼"还是"长期可用",别为了临时需求直接上高套餐。
6 常见排查顺序
配置这类链路,出问题最多的地方其实不在 cpolar 本身。按下面这个顺序查,能省不少时间:
- 服务本身活着吗:回到
127.0.0.1:端口单独访问,确认后端服务正常 - 代理转发对吗:打开统一入口,看能否分流到两个服务,注意
proxy_pass斜杠和Host头 - 清单页只读吗:确认清单页没有任何写入入口
- cpolar 服务起来了吗:
curl -s http://127.0.0.1:9200看本地 Web UI - 隧道在线吗:在线隧道列表里看是否生成了公网地址
- 公网地址打开的是清单吗:最后再确认一遍,别把服务面板暴露出去
按这个顺序走,大部分问题都能定位到具体是哪一层,而不是笼统地觉得"cpolar 又不行了"。
7 总结
到这里,原本散成一堆内网端口的东西,已经被收敛成了一个统一入口,外加一份只读的访问清单;再用 cpolar 把清单开到公网,异地同事拿着一个地址就能看,不用连进内网。
关键步骤就三步:
- 用反向代理把多个本地服务收敛到一个入口,按路径或按域名分流
- 把服务清单做成只读页面,坚决不挂任何写入接口
- 用 cpolar 只映射这份只读清单,而不是整个入口或服务面板
后续要扩展的话,思路也是顺着这条线走:加服务时只需要在代理配置里多一条上游、在清单数据里多一条记录;要长期稳定访问清单,再按需升级到固定地址方案。这样做的好处是,你的内网始终只对外露出"一页纸",而不是一整套可操作的入口——想看清单的人看到清单,能改配置的人始终只有你自己。
说明:本文是「隔离环境中的可执行实验提纲」,文中所有配置片段均为语法正确的骨架,用于说明结构与字段关系,并非在真实机器上安装、启动后得到的实测快照。文中不包含任何实测吞吐、延迟或并发数值。请在自己的环境中补齐配置并实际验证后再使用。