Show a local dev server to a client with Tailscale

Christian Drexler

A client asks to see the page before it goes live. The page is a dev server on my laptop on port 3000, and the honest options used to be: deploy a preview somewhere, share my screen, or wrestle with a tunnel service. Tailscale has two commands that cover this, and they differ in exactly one thing: who can open the link.

tailscale serve --bg 3000      # HTTPS link for devices in my network
tailscale funnel --bg 3000     # HTTPS link for anyone on the internet
tailscale serve status         # what is currently shared
tailscale serve reset          # all of it off

serve: for people inside your network

tailscale serve takes a local port and publishes it over HTTPS under the machine's name, to devices in your own Tailscale network only. The certificate comes from Let's Encrypt through Tailscale, so the browser shows a padlock and no warning. The URL is https://<machine>.<your-tailnet>.ts.net, and anyone logged in to the same network can open it: my phone on the couch, a colleague who is already in. Nobody else even resolves the name. I use this constantly for checking a layout on a real phone without typing an IP and a port.

funnel: for people outside it

tailscale funnel is the same thing with the door open to the public internet. The link works for anyone, no Tailscale account, no app. Traffic enters through Tailscale's relay servers and ends on my laptop, where TLS terminates, so the relay carries ciphertext.

Two preconditions, both one-time: HTTPS certificates and MagicDNS have to be enabled for the network, and the policy file needs a node attribute that allows Funnel on this machine. The first time you run it the CLI prints the exact lines to add, or a link to the admin console that does it for you. On the public side only ports 443, 8443 and 10000 are allowed; the local port can be anything.

Which one

Serve when the viewer has Tailscale: my own devices and the people I work with daily. Funnel when they do not: a client, a reviewer, anyone who should click a link and see a page. Funnel is the one I reach for less often and switch off sooner.

What Funnel is not: a staging environment. The link lives as long as my laptop is awake and the dev server is running. Lid closed, link dead. Bandwidth through the relay is modest, fine for a page walk-through, wrong for a launch or a video upload. And the name derives from the machine name, so it is not secret; if the content matters, the dev server's own login stays in front of it.

Dev server quirks

The proxy forwards to 127.0.0.1:3000 with the public hostname in the Host header. Most servers do not care. Vite does, and refuses with a blocked-host message until the name is listed in server.allowedHosts; newer Next versions want the origin in allowedDevOrigins. Hot reload works through both commands, which still surprises me a little.

Turning it off

--bg keeps the share alive in the background after the command returns, which is what you want for a day of back-and-forth. For a one-hour call I drop the flag: the command then runs in the foreground, and closing the terminal ends the share with it. Either way, tailscale serve reset clears every serve and funnel entry on the machine, and tailscale serve status afterwards should print nothing. I run the status line after every demo, because a forgotten public link to a dev server is the kind of thing you find out about from a stranger.