TSLinkTSLink Docs

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

View as Markdown

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:

bash
tslink add myapp --proxy localhost:3000 --allow you@example.com
tslink url myapp --wait

Replace 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):

bash
tailscale serve --bg --set-path /app 3000

People hit the same few failures, and each of these is a public report:

What you seeReport
GET /@vite/client 404 and ERR_TOO_MANY_REDIRECTSFarplane-UI issue 24, a Vite app behind a path prefix
Assets (JS, CSS, API calls) fail to loadHolyClaude 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 displayedtailscale issue 12413
Redirected to /login.html without the /adguard prefix, locked outReddit, 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 /app does 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 of http://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 base for deployment: with a nested public path, asset paths are rewritten accordingly in vite build output (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.

  • 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 3000 serves 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.net names.

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.

Table of Contents