Tailscale serve vs funnel: which to use
Tailscale serve puts HTTPS on a tailnet-only URL. Funnel opens that same port to the public internet. Which to use, and the policy gate that blocks funnel.
tailscale serve vs funnel: who can reach the URL
The difference between tailscale serve and tailscale funnel is the audience, and nothing else. serve puts an HTTPS (hypertext transfer protocol secure) front end on a local port and publishes it to your tailnet only. funnel publishes that same local port to the whole public internet, through relay servers Tailscale runs. The two commands take the same flags and the same targets. One word stands between a private dashboard and a world-reachable one.
Both hand you a certificate that browsers already trust, on a name ending in ts.net, and neither needs an inbound port open on your VPS firewall. Your tailscaled daemon already holds a connection out to the tailnet, so traffic arrives over that instead. Getting a server onto the tailnet is one job, and running a VPS as a Tailscale exit node or advertising a subnet router for a private network covers that ground. Publishing a service that is already on the tailnet is this one.
What you need before either command works
- Tailscale 1.38.3 or newer on the VPS, logged in to your tailnet. Check with
tailscale versionandtailscale status. - MagicDNS enabled. MagicDNS is Tailscale's built-in DNS (domain name system), and it is what gives the machine a name such as
blog-vps.your-tailnet.ts.netinstead of only a100.xaddress. - HTTPS certificates enabled for the tailnet, on the DNS page of the admin console. Without that there is no certificate to put in front of your port.
- For
funnelonly, thefunnelnode attribute in the tailnet policy file. This is where most first attempts stop, and it is covered below.
Every command here starts with sudo, because the CLI talks to tailscaled through a socket only root may write to. Grant one user the right to skip that:
sudo tailscale set --operator=$USERPublish to your tailnet with tailscale serve
Point serve at a local port and it does the rest.
sudo tailscale serve 3000Available within your tailnet:
https://amelie-workstation.pango-lin.ts.net
|-- / proxy http://127.0.0.1:3000
Press Ctrl+C to exit.The bare 3000 is shorthand for http://127.0.0.1:3000. Tailscale listens on port 443 of the machine's tailnet address, terminates TLS (transport layer security) with the ts.net certificate, and forwards plain HTTP to your local port. Your application never has to know a certificate exists, which is the main reason to reach for this in front of an admin panel you would otherwise leave on plain HTTP.
Now read the last line: Press Ctrl+C to exit. The command runs in the foreground, and the mapping lives inside that process. Close the terminal and the URL stops working, because nothing was written to disk. Add --bg and the mapping goes into the node's serve config, which survives both the terminal and a reboot.
sudo tailscale serve --bg 3000Serve takes more than a port number. --set-path mounts a service under a subpath, so several apps can share one hostname:
sudo tailscale serve --bg --set-path=/grafana 3000
sudo tailscale serve --bg --set-path=/metrics 9090A target can also be a directory of static files, or a backend that already speaks TLS with a certificate you do not want checked:
sudo tailscale serve --bg /srv/reports
sudo tailscale serve --bg https+insecure://localhost:8443It is not limited to HTTP either. --tcp=<port> forwards a raw TCP (transmission control protocol) stream, and --tls-terminated-tcp=<port> ends TLS at your node and passes the plaintext on, which puts a trusted certificate in front of a service that does not speak HTTP at all:
sudo tailscale serve --bg --tls-terminated-tcp=443 tcp://127.0.0.1:9899Why does funnel say the node attribute is not set?
Funnel is off for an entire tailnet by default. The first run prints this and stops:
Funnel not available; "funnel" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.The command was correct. The tailnet policy has not given this node permission to publish, so the client refuses before it ever contacts a relay. Edit the tailnet policy file in the admin console, under Access Controls, and add the attribute:
"nodeAttrs": [
{
"target": ["autogroup:member"],
"attr": ["funnel"],
},
],autogroup:member grants it to every member of the tailnet. If only one machine should ever publish, tag that machine and target the tag instead, for example tag:public. Save the policy, then run the funnel command again.
If your account is a tailnet admin, recent clients offer a shortcut: the CLI prints a consent URL on login.tailscale.com, and following it enables HTTPS certificates and adds the attribute for you. If you are not an admin, that URL will not help you. Someone with policy access has to make the edit.
Publish to the internet with tailscale funnel
Once the attribute is set, the command is the one you already know with a different verb.
sudo tailscale funnel --bg 3000Available on the internet:
https://amelie-workstation.pango-lin.ts.net
|-- / proxy http://127.0.0.1:3000
Press Ctrl+C to exit.Read that first line every single time. Available within your tailnet and Available on the internet are the only visible difference between a private service and a public one, and the commands that produce them differ by one word.
As of August 2026, funnel listens on port 443, 8443 or 10000, and on nothing else. The default is 443, and --https=8443 or --https=10000 are the alternatives. Any other port is refused, because the funnel relays accept connections only on those. That is why a funnel URL is always the bare hostname, or the hostname with :8443 glued on the end.
How do I see what is published right now?
Guessing is how a dashboard stays public for a month. Ask the node instead.
tailscale serve status
tailscale funnel status
tailscale serve status --jsonBoth status commands read the same config, so either one shows you the whole picture. Use the --json form inside a script or a scheduled check, because the plain output is written for people. When nothing is configured you get one line:
No serve configSeeing that after a setup you know worked means the mapping was created in the foreground and the process is gone. Recreate it with --bg.
To remove one mapping, repeat the command that created it and put off at the end. To wipe every serve and funnel mapping on the node, use reset.
sudo tailscale funnel --https=443 3000 off
sudo tailscale serve resetRun tailscale serve status again after either one and read what is left, rather than assuming it did what you meant.
What you get, and what you give up
The gains are real, and they are why people pick this over a reverse proxy.
- A certificate browsers trust, renewed for you. No ACME (automatic certificate management environment) client to install, and no renewal job to forget.
- No inbound port on the VPS firewall.
tailscaleddials out, so a default-deny ufw firewall on your VPS can stay exactly as tight as it was. - No DNS record to buy, point or wait on.
- No port forwarding, which is the whole story on a machine behind NAT (network address translation) rather than a VPS with a public IP.
The costs are just as real, and funnel carries all of them.
- The name is not yours. Public visitors see
host.your-tailnet.ts.net. Funnel has no custom domain support, so you cannot putapp.example.comin front of it. - The path is not yours. Traffic reaches a Tailscale relay first, and the relay proxies the stream to your node over the tailnet. Tailscale states that funnel traffic is subject to bandwidth limits that are not published and cannot be configured, so measure your own throughput before you depend on a number.
- The controls are missing. A reverse proxy you run gives you access logs, rate limits, request size caps and somewhere to put authentication. Funnel gives you a URL. Everything else has to live inside your application.
- The port list is fixed, as above.
Both features also lean on infrastructure Tailscale operates: certificate issuance for the ts.net name, and the funnel relays themselves. If you are weighing a self-hosted Headscale control server, do not assume either one travels with you. Check the release notes for the Headscale version you plan to run.
Which one should I use?
The rule is short.
Use serve for anything internal: admin interfaces, dashboards, a metrics UI you do not want indexed, a staging copy of a site. Membership of the tailnet is the access control, and it is a good one. A device that is not on the tailnet cannot even resolve the name.
Use funnel for a demo link, a webhook receiver that a third party must POST to, or an OAuth callback during development. It is the fastest route to a public HTTPS URL, and one off command ends it. Public means public, though: the hostname is not a secret, and a funnel in front of an app with no login is an open service. Whatever sits behind it must authenticate its own requests, with the same care an exposed Ollama API endpoint needs.
Use a real reverse proxy for anything you would call production. Your domain, your certificate, your logs, your rate limits, and nobody else in the request path. Comparing nginx, Caddy and Traefik as a reverse proxy covers choosing one.
Failure modes, with the strings you will see
Funnel refuses to start. Funnel not available; "funnel" node attribute not set. is a policy problem, not a command problem. Add the funnel attribute to the tailnet policy file, save it, and retry.
It worked, and now tailscale serve status says No serve config. The mapping was created in the foreground and that process ended. Re-run the same command with --bg.
The name resolves but nothing answers. Serve proxies to the target you named, so if nothing is listening there, there is nothing to proxy to. Confirm with ss -ltnp | grep 3000 on the same machine that runs tailscaled. The frequent cause is a container publishing its port on a Docker bridge address instead of 127.0.0.1, which means the host sees no listener where you expected one. How Docker Compose networking works shows where a published port actually lands.
Certificate errors on the ts.net name. HTTPS certificates are most likely not enabled for the tailnet. Turn them on in the admin console, then run the certificate step on its own so its errors are not tangled up with serve output:
sudo tailscale cert your-host.your-tailnet.ts.netThe funnel loads on mobile data but behaves differently from your laptop. Your laptop is on the tailnet, so MagicDNS resolves the name to the 100.x address and you reach the service directly, without touching a relay. That is correct behaviour, and it means your laptop cannot test public reachability at all. Use curl from a machine that is not on the tailnet.
FAQ
What is the difference between tailscale serve and tailscale funnel?
Who can reach the result. tailscale serve publishes a local port at an HTTPS URL that only devices on your tailnet can reach. tailscale funnel publishes the same port at a URL anyone on the internet can reach, routed through relay servers Tailscale runs. The flags and the targets are shared between them. The first output line tells you which one you got: Available within your tailnet or Available on the internet.
Why does tailscale funnel say the node attribute is not set?
Because funnel is disabled for a tailnet until someone enables it. The message is Funnel not available; "funnel" node attribute not set. and it comes from your own client, before any relay is contacted. Add a nodeAttrs entry granting the funnel attribute to autogroup:member, or to a tag if only one machine should publish, in the tailnet policy file under Access Controls. A tailnet admin can instead follow the consent URL the CLI prints.
Which ports can Tailscale Funnel use?
Only 443, 8443 and 10000. The default is 443, and you pick another with --https=8443 or --https=10000. This is a limit of the funnel relays, not of your server, so no firewall change or configuration change on the VPS lifts it. tailscale serve has no such restriction, because it never leaves your tailnet.
Does a serve or funnel URL survive a reboot?
Only if you used --bg. Without it the command runs in the foreground, prints Press Ctrl+C to exit., and the mapping disappears along with the process. With --bg the mapping is written into the node's serve config and comes back with tailscaled after a reboot. Check with tailscale serve status, which prints No serve config when nothing is set.
Is it safe to leave a funnel running?
It is safe in the transport sense: the connection is HTTPS and no port is open on your firewall. It is not safe in the sense people usually mean, because the URL is public, so the application behind it is public. Leave a funnel up only in front of something that authenticates its own requests, and take it down when the demo or the webhook test is over, using the command that created it with off on the end.
Sources for the command behaviour above: the Tailscale Serve and Funnel documentation and CLI reference at tailscale.com/docs.