tailscale serve --set-path broke my app? Use a name
Why mounting an app under /app breaks /assets, /@vite/client and login redirects, and two ways out: make the app and its upstream agree on a prefix, or serve it at / on its own name.
Last updated on
Mounting an app under /app breaks it because many web apps write root-relative URLs such as /assets/index.js, /@vite/client or /login. Serve strips /app before forwarding, and the browser requests those links from the root of the host, outside your mount, so they 404 or redirect somewhere else. There are two ways out: make the app and its upstream agree on the prefix (an untested direction, described below), or give the app its own name and serve it at /. For the second one, with an app on port 3000:
tslink add myapp --proxy localhost:3000 --allow you@example.com
tslink url myapp --waitReplace the login with your Tailscale account. The URL has the form https://<name>.<tailnet>.ts.net, with the app at / on port 443. Remove it later with tslink remove myapp. TSLink is an independent project, not made by Tailscale.
What breaks and what you see
The mount comes from a command like this one, where --set-path mounts the service under a URL path (tailscale serve reference):
tailscale serve --bg --set-path /app 3000People hit the same few failures, and each of these is a public report:
| What you see | Report |
|---|---|
GET /@vite/client 404 and ERR_TOO_MANY_REDIRECTS | Farplane-UI issue 24, a Vite app behind a path prefix |
| Assets (JS, CSS, API calls) fail to load | HolyClaude issue 64, worked around by going back to a root mount |
| "frontend apps assume the path starts at /" | tailscale issue 16389 |
| Only part of the page is displayed | tailscale issue 12413 |
Redirected to /login.html without the /adguard prefix, locked out | Reddit, r/Tailscale, May 2023 |
| "That's just what happens when you try to run web apps on paths" | Reddit, r/Tailscale, July 2025 |
These are user reports, not a Tailscale statement about which apps are affected. The pattern across them is the same: the app was written to run at the root of its host.
What Serve does with the prefix
Serve trims the mount point before it forwards a request: in Tailscale's v1.104.1 source, serve.go strips the prefix with http.StripPrefix and the comment "Trim the mount point from the URL path before proxying". So /app/assets/index.js reaches your app as /assets/index.js, and the app, which knows nothing about /app, writes root-relative links that miss the mount. The upstream URL you give Serve is joined with the remaining path (same file, lines 976-988), and the CLI accepts a full URL that includes a path, such as http://localhost:3000/foo.
Direction 1 (untested): make the app and the upstream agree on a prefix
If you want one name with several paths, the app has to generate prefixed links and the upstream URL has to carry the prefix back to it, because Serve removes it. Most frameworks have a base-path setting; in Vite it is base. Two cautions, neither of which we have tested end to end:
- Setting only
base: '/app/'while Serve strips/appdoes not work. Vite's development server redirects a request for/to the base and returns 404 for unprefixed paths such as/@vite/client(Vite 8.3.5 base middleware). The upstream would have to receive the prefix, for example a Serve target ofhttp://127.0.0.1:3000/app/, and the dev server's Host allowlist still applies (see the Vite "Blocked request" page). - The Vite documentation describes
basefor deployment: with a nested public path, asset paths are rewritten accordingly invite buildoutput (Public Base Path). That does not establish how the development server behaves behind Serve.
Treat this as a direction to try, not a verified recipe. It also works only for apps that expose such a setting, and server-generated redirects or links can still ignore it. The 2023 Reddit thread in the table describes the other workaround for an app without one: a reverse proxy that rewrites its responses, which the poster said is not a general solution.
Direction 2: give the app its own name and serve it at /
When each app has its own name, no app needs to know about a prefix. TSLink does this by running each registered app as its own tsnet node, with the app at / on port 443. Several apps on one machine explains the model and how it compares with Tailscale Services and Docker sidecars; share one app lists what each node exposes.
If you had set a base or a path-prefixed Serve target for the old mount, return to the default /. A dev server may also check the Host header of the new address; the Vite "Blocked request" page covers that case.
Two things change when you split one origin into several names. If browser JavaScript reads API responses from another app hostname, configure that API's CORS policy for the frontend origin, or route those requests through a same-origin proxy. This is an architectural consequence of the browser's CORS protocol, not a tested TSLink result. eido issue 178 is one example of a project that deliberately splits a single ts.net origin by path. Also, registration is per app: add, URL and remove are separate commands for each name.
When not to use TSLink
- The app has a base-path setting and one origin is what you want. If a frontend and its API must share an origin, keep path routing and set the base path as in Direction 1, after verifying it yourself.
- You only have one app. Plain
tailscale serve 3000serves it at the root of the device name. See when you need TSLink. - You want Tailscale to manage the names across hosts. That is what Tailscale Services is for; it needs a tagged host and an administrator.
- You need the app behind your own domain. TSLink only produces
*.ts.netnames.
Related pages
- Several apps on one machine, one ts.net name each: the model, the Services comparison and billing.
- Share an app, not your whole machine: per-app access rules and what each node exposes.
- Use your coding agent's web UI from your phone: the same one-name-per-app approach for dev servers opened on a phone.
- Comparisons: TSLink, TSDproxy, tsnsrv and other tools.
Official Tailscale and Vite sources linked above: accessed 2026-10-22. TSLink behavior: v0.2.0. TSLink is an independent project by anydoor7, not made or endorsed by Tailscale.
Several apps, one machine
Native tailscale serve gives one machine one MagicDNS name. See how Tailscale Services, a TSLink node per app, Docker sidecars and TSDproxy each give apps their own names.
Vite Blocked request
Vite rejects ts.net hosts with Blocked request. This host is not allowed. Add .ts.net to server.allowedHosts, or open the dev server through a TSLink address.