Troubleshooting
Start with tsp doctor — it diagnoses the most common setup problems and prints
the exact fix link for each. The map below points to the right section:
Works from my phone but not my Mac (MagicDNS)
From the host, MagicDNS resolves <node>.ts.net to your tailnet IP, so the
request may be answered locally instead of traversing the public Funnel.
- Test from outside your tailnet — a phone on cellular, or another network.
- Force the public ingress from the host:
PUBIP=$(dig +short <node>.ts.net @1.1.1.1 | head -1) curl -s -i --resolve "<node>.ts.net:443:$PUBIP" "https://<node>.ts.net/<slug>/"
A page loads without styles
Fixed by cookie route-affinity: visiting …/<slug>/ sets a tsp_route cookie so
that tab’s prefix-less requests (/_next/..., /api/..., HMR) reach the right
backend.
- Make sure you opened it via its
/<slug>/URL (that’s what sets the cookie). - Don’t actively use two different apps in the same browser at once — affinity
is per-browser. Use separate browser profiles, or re-visit the
/<slug>/URL.
Reaching services from a container (“Failed to resolve …ts.net”)
This error means the container’s DNS can’t resolve the name. The fix depends on
where the container runs and which mode you used — *.ts.net resolves two
different ways:
- Funnel (public, default) — the name is in public DNS; any host resolves it.
- Serve (
--private) — the name only resolves via MagicDNS inside your tailnet, pointing at a100.xaddress only tailnet members can route.
Container on the same machine as tsp (Docker Desktop)
Skip DNS — talk to the proxy directly by path. Bind it to a reachable address:
tsp --bind 0.0.0.0# inside the container — routes /<slug>/ to your local dev server:
curl http://host.docker.internal:8443/<slug>/ # Docker Desktop
# Linux: docker run --add-host=host.docker.internal:host-gateway ...container → host tsp → 127.0.0.1:<service>, no DNS or tailnet involved.
0.0.0.0 exposes the proxy to your LAN — bind a specific interface (e.g. the
docker bridge --bind 172.17.0.1) to narrow it. host.docker.internal only
points at the same host, so this does not work for a remote pod.
Container on a remote host / Kubernetes (the usual cause)
A remote pod can’t reach host.docker.internal, so use the exposure URL — but how
you make it resolve depends on the mode.
Funnel (public, default): the name is in public DNS, so a pod with working
internet DNS should already resolve it. If the pod’s resolver is locked down, pin
the name to the public Funnel ingress IP (hostAliases is the k8s --add-host):
dig +short your-node.ts.net @1.1.1.1 # → public ingress IP, e.g. 209.177.145.192# Kubernetes pod spec
spec:
hostAliases:
- ip: "209.177.145.192"
hostnames: ["your-node.ts.net"]# plain Docker on a remote host
docker run --add-host "your-node.ts.net:209.177.145.192" ...When the remote host is itself on the tailnet (MagicDNS shadows the name)
The subtle case. If the consuming host runs Tailscale with MagicDNS enabled (the
default, --accept-dns=true), Tailscale’s resolver 100.100.100.100 answers
*.ts.net with the node’s tailnet 100.x address — not the public Funnel
ingress. Traffic then crosses the tailnet to your node’s tailscaled, which only
serves that path if you set up Serve; a Funnel-only node won’t answer.
Containers on that host fall back to public DNS, which may still be negatively
cached as NXDOMAIN:
nslookup your-node.ts.net 8.8.8.8 # NXDOMAIN (not cached yet)
nslookup your-node.ts.net 100.100.100.100 # resolves via MagicDNStsp doctor prints an advisory magicdns note whenever the node has accept-dns
on, as a reminder of this gotcha.
The Funnel name is in public DNS, so stop MagicDNS from shadowing it on the
consuming host (the remote one — not the machine running tsp):
tailscale set --accept-dns=false # use public DNS for *.ts.net; re-enable with =true
dig +short your-node.ts.net @8.8.8.8 # now confirms → 209.177.145.192Prefer tsp to do it on start (opt-in, off by default)? Pass tsp --accept-dns=false.
It persists after exit — revert with tailscale set --accept-dns=true.
Don’t want to change the host’s DNS? Fix only the container — point it at MagicDNS, or pin the public IP:
docker run --dns 100.100.100.100 ... # resolve like the host
docker run --add-host your-node.ts.net:209.177.145.192 ... # or skip DNS entirelyWhy tsp doesn’t set --accept-dns=false for you: it’s a global, persistent,
machine-wide Tailscale setting that disables MagicDNS for every *.ts.net name
(breaking resolution for your other tailnet nodes), and it belongs on the
consumer host — usually a different machine from the one running tsp. A dev
proxy silently changing your system DNS would be surprising and out of scope, so
it’s a deliberate, reversible step you run yourself.
Serve (--private): the name and its 100.x address only work from inside the
tailnet — there’s no public IP to point at. The remote pod must join the
tailnet: run a Tailscale sidecar (or the
Tailscale Kubernetes operator ). Once
it’s on the tailnet, MagicDNS resolves the name and 100.x routes normally. If the
pod can already route 100.x, map the name to the tailnet IP instead
(tailscale ip -4):
docker run --add-host "your-node.ts.net:100.x.y.z" ...Rule of thumb: remote container that stays off the tailnet → use Funnel and resolve the public name. Remote container you can put on the tailnet → use Serve with a Tailscale sidecar and let MagicDNS do its job.
No services found
tsp only registers known web runtimes within the port range by default.
tsp --ports 3000-9000 # widen the range (single port: --ports 4000)
tsp --runtimes node,bun,python # add runtimes
tsp --all # every listener, incl. unrecognized binariesThe same project shows on multiple ports / a slug has a -<port> suffix
Expected. Within one project folder, tsp keeps the process on the lowest port
as the “main” service (clean slug) and gives any other process in the same folder
a -<port> suffix so it stays reachable — e.g. bun dev on :3087 → myapp/,
an aux tool on :4983 → myapp-4983/. A single process on several ports is
collapsed to its lowest port. Two distinct projects sharing a folder name also
get a -<port> suffix. Run tsp list for the canonical slugs.
lsof not found (macOS/Linux)
Install it: apt install lsof / dnf install lsof. macOS ships it.
Funnel still on after a hard kill
tsp resets on Ctrl-C. After kill -9, clear it manually:
tsp reset # or: tsp reset --private (for Serve)
tailscale serve status # should print "No serve config"502 upstream error
The registered dev server isn’t accepting connections (crashed or exited between
scans). The body names the failed 127.0.0.1:<port>; it clears on the next scan
once the server is back.
tsp start --bg runs detached and logs to ./tsp.log. Stop it by kill-ing
the printed pid, then tsp reset to be sure the entry is down.