Behind a reverse proxy
Let a local reverse proxy (Traefik, nginx, …) reach tailnet backends through TailMux — no system-wide Tailscale, no kernel route.
View as MarkdownThe problem#
A reverse proxy forwards each request by dialing a network address; it does not speak a forward-proxy protocol for its upstreams. So pointing it straight at a tailnet host like api.work.ts.net only works when the machine already has a route into the tailnet — e.g. the official Tailscale client running. TailMux stays at the proxy layer and creates no such route, so a naïve upstream just fails to connect.
How it works#
TailMux's router on 127.0.0.1:43100 accepts ordinary HTTP requests and routes them by their Host header (port included) to the profile that owns the suffix. That makes it usable as a reverse-proxy backend: point the upstream at the TailMux loopback and set the request Host to the real tailnet target.
1reverse proxy2 | upstream = 127.0.0.1:431003 | Host: api.work.ts.net:80804 v5TailMux router 127.0.0.1:431006 | hostname suffix match -> work profile7 v8api.work.ts.net:8080 (resolved + dialed inside the tailnet)host.docker.internal:43100 (Docker Desktop and OrbStack provide it). TailMux resolves the tailnet hostname and routes it, so the container needs no tailnet DNS of its own.Traefik#
Point the service at the loopback and add a headers middleware that forces the upstream Host:
1http:2 services:3 api:4 loadBalancer:5 servers:6 - url: http://host.docker.internal:431007 middlewares:8 api-host:9 headers:10 customRequestHeaders:11 Host: "api.work.ts.net:8080"Attach the api-host middleware to the router that uses the api service. To generate this from a template, keep one variable for the upstream and one for the Host, so the same config switches between a direct route and the TailMux loopback.
nginx#
1location /api/ {2 proxy_pass http://127.0.0.1:43100;3 proxy_set_header Host api.work.ts.net:8080;4}Notes#
- The profile that owns the suffix must list it under
suffixes, and — when the backend sits behind a tailnet subnet router — setaccept_routes: true. - The profile must be logged in for the dial to succeed.
- This routes plain-HTTP upstreams by
Host. For TLS-to-backend or raw TCP (databases, SSH) use tailmux connect / tunnel instead.