tailscale serve 子路径导致 JS/CSS 404 怎么办
应用挂在 /app 下后 /assets、/@vite/client 和登录重定向为什么会失效,以及两条出路:让应用和上游就前缀达成一致,或给它独立名字并挂在 / 下。
最后更新于
把应用挂在 /app 下会失效,原因是许多 Web 应用写出的是以根开头的 URL,例如 /assets/index.js、/@vite/client 或 /login。Serve 会在转发前去掉 /app,浏览器又从主机根路径请求这些地址,落在你的挂载点之外,于是返回 404,或者跳转到别处。 出路有两条:让应用和上游就前缀达成一致(未测试的方向,见下文);或者给应用一个独立名字,挂在 /。第二条做法如下,假设应用在 3000 端口:
tslink add myapp --proxy localhost:3000 --allow you@example.com
tslink url myapp --wait把登录名换成你的 Tailscale 账号。地址形如 <name>.<tailnet>.ts.net,应用位于 443 端口的 /。之后用 tslink remove myapp 移除。TSLink 是独立项目,并非由 Tailscale 出品。
哪里坏了,会看到什么
挂载来自类似下面的命令,其中 --set-path 把服务挂在某个 URL 路径下(见 tailscale serve 参考)。执行之后,应用的地址就变成设备名加上 /app:
tailscale serve --bg --set-path /app 3000大家遇到的故障集中在下面几种,每一条都有公开的报告,下表给出现象和出处:
| 现象 | 报告 |
|---|---|
GET /@vite/client 404 和 ERR_TOO_MANY_REDIRECTS | Farplane-UI issue 24,路径前缀后面的 Vite 应用 |
| JS、CSS、API 调用等资源加载失败 | HolyClaude issue 64,退回根路径挂载后绕过 |
前端应用默认路径从 / 开始 | tailscale issue 16389 |
| 页面只显示了一部分 | tailscale issue 12413 |
登录后跳到不带 /adguard 前缀的 /login.html,把自己锁在外面 | Reddit r/Tailscale,2023 年 5 月 |
| 把 Web 应用放到路径下运行,本来就会这样 | Reddit r/Tailscale,2025 年 7 月 |
这些都是用户报告,并非 Tailscale 对受影响应用范围的说明。它们的共同点是:应用在编写时就假定自己位于主机根路径。
Serve 怎样处理前缀
Serve 在转发请求之前会去掉挂载前缀。Tailscale v1.104.1 的源码里,serve.go 用 http.StripPrefix 去掉前缀,注释写的是转发前先把挂载点从 URL 路径中去掉。所以 /app/assets/index.js 到达应用时是 /assets/index.js,应用并不知道 /app 的存在,写出的仍是以根开头的链接,落在挂载点之外。你给 Serve 的上游 URL 会与剩余路径拼接(同一文件的 976 到 988 行),命令行也接受包含路径的完整 URL,路径会出现在转发给上游的请求里。
方向一(未测试):让应用和上游就前缀达成一致
想保留同一个名字、用多个路径,就要让应用生成带前缀的链接,同时上游 URL 要把前缀带回给应用,因为 Serve 会把它去掉。多数框架都有 base path 设置,Vite 里是 base。有两点需要注意,我们都没有端到端测试过:
- 只设置
base: '/app/',而 Serve 又去掉/app,是行不通的。Vite 开发服务器会把对/的请求重定向到 base,对/@vite/client这类不带前缀的路径返回 404(见 Vite 8.3.5 的 base 中间件)。上游必须收到前缀,例如把 Serve 的目标写成本机 3000 端口并带上/app/路径,开发服务器的 Host 白名单仍然生效(见 Vite Blocked request 页面)。 - Vite 文档里的
base讲的是部署:在嵌套的公共路径下,vite build的产物会相应改写资源路径(见 Public Base Path)。这并不能说明开发服务器挂在 Serve 后面时的行为。
请把这当作可以尝试的方向,而不是验证过的配方。它也只对提供此类设置的应用有效,服务端生成的重定向或链接仍可能忽略它。表中 2023 年的 Reddit 帖子描述了没有这类设置时的另一种办法:在前面加一层改写响应的反向代理,发帖人说这种办法并不通用。
方向二:给应用独立名字,挂在 /
每个应用有自己的名字之后,就没有应用需要知道前缀。TSLink 把每个注册的应用作为独立的 tsnet 节点运行,应用位于 443 端口的 /。一台机器多个服务解释了这个模型,以及它与 Tailscale Services、Docker sidecar 的对比;分享一个应用列出了每个节点暴露的内容。
如果之前为旧的挂载设置过 base 或带路径前缀的 Serve 目标,请改回默认的 /。开发服务器还可能检查新地址的 Host 头,这种情况见 Vite Blocked request 页面。
把一个源拆成多个名字,有两点会变化。不同名字就是不同的源,这就是上面说的跨源读取的来由。如果浏览器 JavaScript 要读取另一个应用主机名上的 API 响应,就要为前端所在的源配置该 API 的 CORS 策略,或者让这些请求经过同源的反向代理。这是浏览器 CORS 协议带来的架构后果,并非 TSLink 的测试结果。eido issue 178 是一个有意把单个 ts.net 源按路径拆分的项目例子。此外,注册是按应用进行的,添加、取地址和移除都要对每个名字分别执行。
何时不该用 TSLink
- 应用提供了 base path 设置,而且你想保持同一个源。 前端和它的 API 必须同源时,保留路径路由并按方向一设置 base path,同样请先自己验证。
- 你只有一个应用。 直接执行
tailscale serve 3000,它就挂在设备名的根路径下。参见何时使用 TSLink。 - 希望由 Tailscale 统一管理跨主机的名字。 这正是 Tailscale Services 的用途,它需要带 tag 的主机和管理员权限。
- 需要应用使用你自己的域名。 TSLink 只提供
*.ts.net名字。
相关页面
- 一台机器多个服务,每个都有自己的 ts.net 名字:模型、与 Services 的对比以及计费。
- 分享一个应用,而非整台机器:每个应用的访问规则,以及各节点暴露的内容。
- 在手机上使用编程 agent 的 Web UI:开发服务器在手机上打开时同样适用的做法。
- 工具对比:TSLink、TSDproxy、tsnsrv 及其他工具。
以上 Tailscale 与 Vite 官方来源访问日期:2026-10-22。TSLink 行为以 v0.2.0 为准。TSLink 是 anydoor7 的独立项目,并非由 Tailscale 制作或背书。