又是全让 AI 写的,仅做一点修改,当个记录使用。
里面有些话还是挺弱智的,不代表我自己的观点。
说在前面
我原本把 Open WebUI 部署在云服务器上(2 核 4G、东京、30Mbps),多端互通用得挺爽。但升级到新版本之后,体验直线下降:随便打开一个新页面,侧边栏的 folders 和 chats 要转上一分钟才出来,时不时还给你来个 takes too long to respond。
我所有能力当时都走 API: Embedding 和 Reranking 用的是 BGE-M3 / BGE-Reranker-V2-M3,聊天模型走第三方中转。所以这个卡顿其实不是模型跑不动,更像是新版前端、数据库或者容器资源的问题。
于是就有了这次折腾,目标其实有三个,而且优先级不一样:
- 把卡顿解决掉,顺便把主实例搬到性能强得多的台式机;
- 保留多端(手机、Mac、外网)随时访问的能力 —— 这是我最担心搬回家会丢掉的东西;
- 后续能玩本地大模型,兼顾隐私。
本次折腾概览
- 云服务器当 FRP 中继(frps),台式机当客户端(frpc),外网访问全程经云服务器中转
- 台式机用 Docker Desktop(WSL2 后端)跑 Open WebUI
- 踩了一堆坑:Docker 端口映射失效、Windows Defender 误杀、FRP token 不匹配、手机 502
- 域名复用现有 Caddy,先用临时子域名过渡,不动生产
- frpc 用任务计划程序开机自启,并全程 Portable
- 干掉 Watchtower 的自动更新,冻结版本
- 本地模型:Embedding 换本地 BGE-M3,Reranking 继续留在 API,Chat 折腾了 qwen 一些版本
整体架构
最终跑通的链路是这样的:
手机 / Mac / 外网
↓ HTTPS :443
云服务器 Caddy
↓ 本机 127.0.0.1:13000
云服务器 frps
↓ 加密 FRP 隧道
家里 Windows frpc
↓ 本机 127.0.0.1:8080
Open WebUI 容器(Docker)
为什么是 FRP,不是 Tailscale / ZeroTier
我之前 Tailscale、ZeroTier 都试过,甚至把自己的服务器设成中继节点,还是各种失败 —— 核心原因是我的网络环境 P2P 打洞极不稳定。Persec 也失败,唯一稳的是向日葵,但向日葵实在太毒瘤。
FRP 的思路不一样:它放弃赌 P2P 直连,直接强制走中继。台式机主动连出去连云服务器,访问流量明确经过云服务器。不需要公网 IP、不需要路由器端口映射、CGNAT 也不影响。代价是绕一圈东京,延迟高一点。
一、云服务器装 frps
下载最新版 FRP,装二进制:
sudo install -m 755 frps /usr/local/bin/frps
frps --version
生成一个强随机 Token(别用 123456 这种):
openssl rand -hex 32
写 /etc/frp/frps.toml:
bindAddr = "0.0.0.0"
bindPort = 7000
auth.method = "token"
auth.token = "你的随机Token"
transport.tls.force = true
allowPorts = [
{ start = 13000, end = 13010 },
{ start = 13389, end = 13399 }
]
log.to = "/var/log/frps.log"
log.level = "info"
log.maxDays = 7
systemd 服务是干嘛的
简单说,systemd 就是 Linux 里的"大管家",给 frps 配一个 service 只有一个目的:保活 + 自启。不配它,你 SSH 一断、机器一重启,frps 就没了。配了它就是开机自动跑、崩溃自动重启,可以完全忘掉它的存在。
[Unit]
Description=FRP Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=always
RestartSec=5
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now frps
sudo systemctl status frps
云服务器安全组放行 7000(FRP 控制)、80、443(后面 Caddy 用)。
坑 1:命令粘到提示符里了。 我复制粘贴的时候,把
sudo systemctl enable --now frps后面粘上了一大串终端提示符字符,结果这条启动命令根本没执行成功,查状态一直是inactive (dead)、日志No entries。看着邪门,其实就是命令没跑起来。老老实实单独敲一遍就好。
二、Windows 上的 frpc
FRP 客户端是纯 Portable 软件,就俩文件 frpc.exe + frpc.toml,不写注册表、不装运行库,删文件夹就跟没来过一样。所以我一开始就把它放进我的 C:\Portable\frp(我 C 盘专门有个 Portable 文件夹放绿色软件),而不是教程默认的 C:\frp。
注意:一开始就定好最终路径。 因为后面要用任务计划程序做开机自启,路径变了自启就失效。别配好了再挪。
坑 2:Windows Defender 把 frpc.exe 当病毒删了。 一解压就报威胁(
HackTool:Win32/FRP之类),连原文件都被干掉。这是 Defender 对内网穿透工具的预防性误杀,不是你下到了假货。解法是先把目录加进 Defender 排除项,再重新下载,在被排除的文件夹里面解压:Add-MpPreference -ExclusionPath "C:\Portable\frp"
坑 3:压缩包里的文件别全塞进去。 包里有
frps(服务端,家里用不到)、frpc(客户端,要的)、LICENSE(不用)。只留frpc.exe和frpc.toml。
三、大坑:我压根还没在本机部署 Open WebUI
配到"确认本机 Open WebUI 端口"这一步,在两个终端 docker ps 都失败:服务器上是 permission denied(要加 sudo),Windows 上直接 docker 不是命令。
原因很蠢但很关键:FRP 只是把台式机上已存在的服务转发出去,它不会自动把云端的 Open WebUI 搬到本机。 此前所有东西都还在云服务器上,本机根本没有 Docker、也没有 Open WebUI。这一整段被gpt漏掉了。
于是补课:装 Docker Desktop(WSL2 后端)。这里得诚实说一句:Docker Desktop 不是 Portable,它会装 Windows 程序和服务,不像 FRP 删文件夹就干净。但它是 Win + WSL2 场景下端口、GPU、自启最省心的方案,所以我认了。它和我 WSL 里的开发环境只通过 localhost 端口通信,不共享文件系统,算是最干净的隔离方式。
部署本机 Open WebUI:
docker volume create open-webui-local-data
docker run -d --name open-webui-local --restart unless-stopped `
-p 127.0.0.1:3000:8080 `
-v open-webui-local-data:/app/backend/data `
ghcr.io/open-webui/open-webui:main
坑 4:拉镜像 EOF。 中途报
failed to fetch ... EOF,是代理网络抽风。Docker 分层下载,已完成的层会缓存,直接重跑同一条命令即可,拉一半的碎片它自己会校验丢弃,不留垃圾。
真正的大坑:端口映射死活不生效
容器 healthy 了,但 docker ps 的 PORTS 只显示 8080/tcp,而不是期望的 127.0.0.1:3000->8080/tcp。http://127.0.0.1:3000 打不开。查底层配置:
docker inspect open-webui-local --format '{{json .NetworkSettings.Ports}}'
# {"8080/tcp":[]}
一个空数组。也就是 Docker 收到了 -p 配置(HostConfig.PortBindings 里有),但根本没真正把端口发布出来。
排查过程记录一下,避免以后又走弯路:
- 不是命令写错:
-p 127.0.0.1:3000:8080和-p 3000:8080都试了,都是空数组。 - 不是 VPN :
127.0.0.1是回环,流量根本不出机器,关不关 VPN 无关。 - 不是设置乱改:
NetworkMode=bridge正常、Host networking 没开、Port binding 是默认。 - 重启 Docker Desktop 无效。
结论:当前 Docker Desktop 的端口发布功能整体失效,原因没深究。
解法:干脆绕过端口映射,启用 Host networking。
docker rm -f open-webui-local docker run -d --name open-webui-local --restart unless-stopped ` --network host ` -v open-webui-local-data:/app/backend/data ` ghcr.io/open-webui/open-webui:main这样 Open WebUI 直接用宿主机网络,少了一层映射,访问地址从此变成
http://127.0.0.1:8080(不是 3000 了!后面 FRP 的localPort也得跟着改成 8080)。终于打开了。代价:Host networking 下
8080不再被127.0.0.1限制,理论上局域网设备也能访问。后续可以用 Windows 防火墙挡掉局域网入站,只留本机和 frpc。
四、FRP 打通
frpc.toml 关键段(注意 localPort = 8080):
serverAddr = "<你的服务器公网IP>"
serverPort = 7000
auth.method = "token"
auth.token = "和服务器一模一样的Token"
transport.tls.enable = true
loginFailExit = false
log.to = "C:/Portable/frp/frpc.log"
[[proxies]]
name = "open-webui"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 13000
坑 5:token in login doesn’t match。 frpc 日志一直刷这句。别用肉眼对几十位字符串,最稳的是重新生成一枚 Token,服务器和 Windows 两边同时替换,
frps记得systemctl restart。另外提醒自己:Token 千万别贴到聊天/截图/笔记里,贴了就当泄露,立刻轮换。
frpc 日志出现 login to server success + start proxy success,服务器上 sudo ss -lntp | grep 13000 能看到监听,就说明隧道通了。日志被我重定向到文件了,所以前台窗口"没反应"是正常的,Get-Content ...frpc.log 去看。
坑 6:手机 502,折腾半天发现是 VPN。 服务器本机
curl -I http://127.0.0.1:13000返回 200,说明整条链路是通的。但手机开着 VPN 访问http://<IP>:13000就一直转、最后 502。关掉 VPN、切移动数据,秒开。 教训:HTTP + 裸 IP + 非标准端口这种组合,很多 VPN 节点会抽风。别拿"YouTube 能开"推断"访问自己服务器也没问题",两者路径完全不同。这也是后面必须上 HTTPS 域名的理由 —— 标准HTTPS + 域名 + 443的兼容性好太多。
五、域名 + Caddy + HTTPS
我服务器上早就有 Caddy 在给 域名A 服务(反代到云端旧 Open WebUI 的 localhost:8080)。Caddyfile 极简:
域名A {
reverse_proxy localhost:8080
}
这里的策略很重要:不动生产,用临时子域名过渡。 因为本机是全新空实例,直接把 域名A 切过去,我打开熟悉的域名会看到一个空白 Open WebUI。所以新开一个 local-域名A 指向 FRP,验证 + 迁数据都稳了,再考虑切正式域名,随时能回退。
先备份再改:
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.backup-$(date +%F-%H%M%S)
域名A {
reverse_proxy localhost:8080
}
local-域名A {
reverse_proxy 127.0.0.1:13000
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Cloudflare 这里选 DNS Only(灰云)。 我原来的
域名A也是灰云。对 Open WebUI 我建议保持灰云:聊天内容、上传、流式响应不经过 Cloudflare,少一层影响 WebSocket / 长连接的变量,Caddy 也能直接向 Let’s Encrypt 自动签证书。橙云代理反而要处理 SSL 模式(至少 Full strict)、超时、流式兼容等一堆事,对我没必要。
HTTPS 通了之后,记得删掉安全组里
13000的公网放行。 因为 Caddy 和 frps 在同一台机器,Caddy 走127.0.0.1:13000不经过安全组,外面就再也访问不到裸的IP:13000了。最终对外只留80 / 443 / 7000。
六、frpc 开机自启(任务计划程序)
用任务计划程序,不是"基本任务"。
- 常规:名字随意;勾"不管用户是否登录都要运行"+“最高权限”。
Configure for里没有 Windows 11 选项,选 Windows 10 完全没问题,它只是兼容目标。 - 触发器:启动时,延迟 30 秒(等网络和 Docker 就绪)。
- 操作:程序
C:\Portable\frp\frpc.exe,参数-c "C:\Portable\frp\frpc.toml",起始于C:\Portable\frp(起始于和程序路径不要加引号)。 - 条件:取消"仅在交流电源时启动"、取消网络条件判断(frpc 自己会重连)。
- 设置:失败每 1 分钟重启、重试次数给大点;“如果已在运行则不启动新实例”;取消"运行超过 X 时间就停止"。
配完先把手动跑的 frpc 用 Ctrl+C 停掉,再右键任务"运行",Get-Process frpc 验证。
七、台式机睡眠与 WOL(思路,还没完全落地)
我不想让台式机 7×24 醒着费电。真正需要的是:平时睡眠,外出时用手机/Mac 发个唤醒指令把它叫醒,等 1-3 分钟 Docker/Open WebUI/frpc 自动恢复后再访问。这其实就是当年向日葵开机棒干的事 —— Wake-on-LAN。
关键前提(还得慢慢调):
- 台式机必须有线连路由器(Wi-Fi 唤醒别想);
- 主板 BIOS 开启网络唤醒、关掉 ErP / Deep Sleep 这类会切断网卡待机电的省电项;
- Windows 网卡电源管理里允许 Magic Packet 唤醒;
- 需要一个 24 小时在线、且同局域网的设备来发魔术包(路由器自带 WOL 最省事,或者用 NAS 自建一个带认证的唤醒中继);
- 优先"睡眠唤醒",成功率比"关机唤醒"高。
补一句现实:睡眠后 Docker/frpc 都停了,所以不可能"访问隧道再把它叫醒" —— 唤醒必须靠另一个常在线的设备。
八、数据迁移 + 干掉 Watchtower
盘点云端:数据在 /var/lib/docker/volumes/open-webui/_data,镜像是 open-webui:main,而且还挂着一个 watchtower --cleanup open-webui。
这个 Watchtower 很可能就是"新版之后越来越卡"的元凶。 它盯着 open-webui 容器,配合浮动的
:main标签,会在我不知情时自动拉新版重建。Open WebUI 迭代极快、每次更新多少带点 bug,这种"无感自动追 main"对生产环境是灾难。处理:
sudo docker stop watchtower,以后冻结明确版本号 + 手动更新。而且--cleanup会清旧镜像,翻车了想回退都难,更不该留着。
迁移遵循原则:先备份、迁到本机、验证,全程保留云端可回退。备份时先停 Watchtower、停容器做一致性快照、打 tar 包、马上把云端容器起回来,尽量少中断 域名A。
数据搬到本机、用匹配版本起容器、确认 Chats/Folders/知识库都在之后,就进入过渡状态:
local-域名A → 本机新实例(日常主用)
域名A → 云端旧实例(原样保留,紧急回退)
这段时间日常全切 local,云端只用于查看和备用 —— 别在两边同时写新聊天,否则两份数据库就从此分叉了。
九、本地模型(Ollama)
Ollama 装 Windows 原生版(GPU 调用最省事),Open WebUI 容器通过 host.docker.internal:11434 或(Host networking 下)127.0.0.1:11434 调它。
坑 7:装完
ollama不是命令。 PATH 没刷新,关掉旧 PowerShell 重开一个,或者注销/重启一次就好。
Embedding:换本地 BGE-M3
ollama pull bge-m3
坑 8:
ollama embed命令不存在。 我这版 Ollama 的 CLI 没有embed子命令,别以为模型坏了。直接打 HTTP:Invoke-RestMethod -Uri "http://127.0.0.1:11434/api/embed" -Method Post ` -ContentType "application/json" ` -Body (@{ model="bge-m3"; input="测试" } | ConvertTo-Json)
Open WebUI 里 Embedding Engine 选 Ollama、模型填 bge-m3:latest、API Key 留空即可。
我也不太懂,这和我以前用的那个是否确实有区别?我目前是把大部分常用的 RAG 知识库用它重新跑了一遍,然后尝试性的使用,感觉也没什么问题。有问题再说吧。
Reranking:继续留在 API
Reranker 本身不大,但本地部署更麻烦 —— Ollama 的标准接口是 /chat、/generate、/embed,没把 /rerank 当一等公民,通常得另起一个专门服务(Infinity / TEI 之类),还要看 Open WebUI 版本对本地 rerank endpoint 的支持程度。收益/维护比不划算,所以 BGE-Reranker-V2-M3 我先继续用 API。
Chat 模型:一部血泪史
这是最折腾的部分。先说个关键排查方法:绕过 Open WebUI,直接打 Ollama 的 /api/chat 和 /api/generate 看 prompt_eval_duration / eval_duration,能一刀切开"模型/GPU 慢"还是"Open WebUI 塞了太多东西"。
- qwen3.5:9b —— 删了。 各种鬼畜:简单问题也拒答(“请提出符合法律法规的问题”);更致命的是正常回答结束、甚至没按 Stop,GPU 利用率也永远卡在 93% 左右;Open WebUI 里首 token 前经常干等一分多钟(GPU 满载但 UI 没反应,像在做超长 prompt prefill / thinking)。裸 API 测下来生成其实很快,所以问题出在它的模板 / thinking / 取消链路与当前 Open WebUI 的兼容性上,不值得救。
- qwen2.5:7b —— 快而稳。 纯 New Chat 秒出,结束后 GPU 利用率立刻掉回 0%,没有任何残留。缺点是读不到 Folder 绑定的自动 RAG,但在对话里手动把资料丢给它就能正常调用、也不卡首 token。就是版本有点老。
- qwen3:8b —— 可用。 行为正常,但首字符比 2.5 慢一些,是个 thinking 模型;智力观感更好一点。同样读不到 Folder 自动 RAG。
- 想追新的 27B/35B 级(如 3.6)对 16G 显存不合适,模型文件就 17G+,大概率 CPU 卸载,慢到违背"本地要快"的初衷,直接放弃。
关于 Folder 自动 RAG 读不到: 小模型能读手动附加的资料,却抓不住 Open WebUI 自动注入的 RAG 上下文 —— 因为后者混在系统提示、角色设定、工具定义里,7B/8B 分不清哪些是"必须遵循的事实"。这是能力边界,不是链路故障。
最终的分工
不追求"全本地",按隐私/体验/复杂度分层才最划算:
| 环节 | 方案 |
|---|---|
| Embedding | 本地 Ollama BGE-M3(新建/重建的知识库) |
| Reranking | API BGE-Reranker-V2-M3(暂不本地化) |
| Chat · 简单私密快任务 | 本地 qwen2.5:7b(改写/摘要/翻译/格式化/手动附资料问答) |
| Chat · 稍复杂本地任务 | 本地 qwen3:8b |
| Chat · 普适版助手/自动RAG/工具/重要判断 | 闭源强模型 API |
一句话:本地模型能做到"私密任务秒出、基础能力可靠"就已经很有价值;复杂的自动 RAG + 工具 + 长期记忆那套,继续交给闭源强模型,不算失败,是正确分工。
还没搞 / 之后想搞
- WOL 真正落地(BIOS + 网卡 + 常在线设备发包)
- 本地 Reranker(等确认某个高敏感知识库确实值得全链路本地再说)
- 稳定跑一阵后,把
域名A正式切到本机 FRP 上游,云端降级为轻量备用 - 旧知识库按敏感度分批重建为本地 Embedding
- 试
qwen2.5:14b这类更适配 16G 显存的模型,而不是盲目追榜单 - Windows 防火墙给 Host networking 的
8080加一条局域网入站阻断
我是差点忘了加这一句的 Wenix(其实就是忘了,现在刚刚补),希望你开心~