---
title: "tailscale serve --set-path broke my app? Use a name"
description: "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."
url: "https://tslink.md/docs/serve-set-path-breaks-app"
locale: "en"
product_version: "0.2.0"
source: "https://github.com/anydoor7/tslink/blob/v0.2.0/docs/architecture.md"
---

> Documentation index: https://tslink.md/llms.txt · Installed binary is authoritative: `tslink manifest`.

**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](https://tailscale.com/docs/reference/tailscale-cli/serve)):

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

People 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](https://github.com/ZanarkandTechnologies/Farplane-UI/issues/24), a Vite app behind a path prefix              |
| Assets (JS, CSS, API calls) fail to load                              | [HolyClaude issue 64](https://github.com/CoderLuii/HolyClaude/issues/64), worked around by going back to a root mount                |
| "frontend apps assume the path starts at /"                           | [tailscale issue 16389](https://github.com/tailscale/tailscale/issues/16389)                                                         |
| Only part of the page is displayed                                    | [tailscale issue 12413](https://github.com/tailscale/tailscale/issues/12413)                                                         |
| Redirected to `/login.html` without the `/adguard` prefix, locked out | [Reddit, r/Tailscale, May 2023](https://www.reddit.com/r/Tailscale/comments/137ubdx/tailscale_serve_webapps_how_to_deal_with)        |
| "That's just what happens when you try to run web apps on paths"      | [Reddit, r/Tailscale, July 2025](https://www.reddit.com/r/Tailscale/comments/1lyi5t3/tailscale_serve_path_routing_for_web_apps_like) |

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`](https://github.com/tailscale/tailscale/blob/v1.104.1/ipn/ipnlocal/serve.go#L1228-L1232) 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](https://github.com/tailscale/tailscale/blob/v1.104.1/ipn/ipnlocal/serve.go#L976-L988)), 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](https://github.com/vitejs/vite/blob/v8.3.5/packages/vite/src/node/server/middlewares/base.ts)). 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](https://tslink.md/docs/vite-blocked-request-ts-net.md)).
* The Vite documentation describes `base` for deployment: with a nested public path, asset paths are rewritten accordingly in `vite build` output ([Public Base Path](https://vite.dev/guide/build#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](https://tslink.md/docs/several-apps-one-machine.md) explains the model and how it compares with Tailscale Services and Docker sidecars; [share one app](https://tslink.md/docs/share-one-app.md) 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](https://tslink.md/docs/vite-blocked-request-ts-net.md) 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](https://fetch.spec.whatwg.org/#http-cors-protocol), not a tested TSLink result. [eido issue 178](https://github.com/mgwedd/eido/issues/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 3000` serves it at the root of the device name. See [when you need TSLink](https://tslink.md/docs/when-to-use-tslink.md).
* **You want Tailscale to manage the names across hosts.** That is what [Tailscale Services](https://tailscale.com/docs/features/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.

## Related pages

* [Several apps on one machine, one ts.net name each](https://tslink.md/docs/several-apps-one-machine.md): the model, the Services comparison and billing.
* [Share an app, not your whole machine](https://tslink.md/docs/share-one-app.md): per-app access rules and what each node exposes.
* [Use your coding agent's web UI from your phone](https://tslink.md/docs/agent-ui-phone.md): the same one-name-per-app approach for dev servers opened on a phone.
* [Comparisons](https://tslink.md/docs/comparisons.md): 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](https://github.com/anydoor7/tslink), **not made or endorsed by Tailscale**.
