Use Tailscale Funnel and serve
The Tailscale CLI allows you to serve content to your tailnet and beyond. With Tailscale Funnel, you can even expose individual folders or services to the public internet over HTTPS. We’ve heard from lots of Tailscale users about how they’re using serve
and Funnel, and we have collected these examples to help inspire you to use Funnel in new and interesting ways.
If you want to expose the development copy of your website to the world to test your OpenGraph metadata changes, you can do that with Funnel. If you want to share a demo of a web application with a client, you can do that with Funnel. If you want to quickly test changes to a webhook receiver without waiting for the cloud to converge on every change, you can do that with Funnel.
Conceptually, if you have a HTTP service listening on your local machine, you can use the Tailscale client to expose it to the internet with Funnel.
Use cases and examples
Simple fileserver on tailnet
In this example, we will explore how to use serve
with a path
to create a simple fileserver. Using tailscale serve
as a fileserver is often much more efficient than transferring through a third-party service and more convenient than using something like Python’s http.server.
To see how we can go about this, we first need some files to serve. For this example will create those from scratch, but feel free to use existing files on your local machine you would like to share.
$ mkdir /tmp/public
$ echo "Hello World" > /tmp/public/hello.txt
$ echo "Pangolin" > /tmp/animal.txt
Now, we can mount our file at /animal.txt and our directory at /public.
$ tailscale serve https /public /tmp/public
$ tailscale serve https /animal.txt /tmp/animal.txt
The status will confirm the previous command and provide the hostname that Funnel configures for this node. You can see that this is limited to our tailnet only.
$ tailscale serve status
https://example.pango-lin.ts.net (tailnet only)
|-- /public/ path /tmp/public
|-- /animal.txt path /tmp/animal.txt
For the purpose of this guide, we will use curl to confirm the URLs work. You can see that we get a directory listing when we request the /public.
$ curl -L https://example.pango-lin.ts.net/public
<pre>
<a href="hello.txt">hello.txt</a>
</pre>
$ curl -L https://example.pango-lin.ts.net/animal.txt
Pangolin
Serving a static site
This example also uses serve
with a path
, this time to serve a static site.
To get started, let’s create some files as an example of what a static site would consist of; an index file and some assets. Hand-coded HTML and CSS might be all you need, or you can use one of the many static site generators that exist.
/tmp/static-site/index.html
<html>
<head>
<title>Hello World</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
/tmp/static-site/styles.css
*,
html {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: monospace;
font-size: 10vw;
text-transform: uppercase;
}
body {
position: absolute;
left: 50%;
top: 25%;
transform: translate3d(-50%,-50%,0);
overflow: hidden;
}
h1 {
position: relative;
top: 2em;
animation: slide-up 3s infinite;
}
@keyframes slide-up {
0% {top: 2em}
50% {top: 0em}
100% {top: 2em}
}
Now, let’s serve our static site to just our tailnet.
$ tailscale serve https / /tmp/static-site
The status will confirm the previous command and provide the hostname that Funnel configures for this node. Here, we can see it’s on the tailnet only.
$ tailscale serve status
https://example.pango-lin.ts.net (tailnet only)
|-- / path /tmp/static-site
You can now open the URL in your browser to confirm everything is working.

We can optionally enable Funnel to expose our static site to the internet.
$ tailscale funnel 443 on
$ tailscale funnel status
https://example.pango-lin.ts.net (Funnel on)
|-- / path /tmp/static-site
Expose your development server to the public
Having a route accessible with Funnel means that other users on the internet can reach out to and interact with your development copy of a website. If you are working on a blog post and want to share a draft for review, you can make your development site available to the public through Funnel.
You can use Funnel to expose your development server on your machine and then give that URL to others as needed.
Assuming you have a local HTTP server running on port 3030
, enable serving port on your tailnet:
$ tailscale serve https / http://localhost:3030
Then enable Funnel:
$ tailscale funnel 443 on
Finally, get the public URL for your machine:
$ tailscale funnel status
https://example.pango-lin.ts.net (Funnel on)
Open the URL in your browser to confirm that everything is working correctly.
As long as your development machine is turned on and connected to Tailscale, the data will be routed to and from your development server with Funnel.
Sharing a Funnel node
One of the nice things about Funnel is having a predictable, stable DNS name, like example.pango-lin.ts.net
. This allows you to set or share your DNS name once and have it easily accessible any time you turn your funnel on.
But what if you want to share the Funnel DNS name with multiple collaborators or colleagues? This can come up if you’re configuring a backend like GitHub with a development webhook URL and wish to keep that URL stable, no matter which developer is currently testing the webhook APIs.
Our recommendation for sharing a Funnel node is to set up a node with the desired name. Let’s call it github-hooks-dev.corp-xyz.ts.net
. Optionally, turn on Tailscale SSH to make it easier to connect to.
Turn on Funnel on this node, for example:
$ tailscale serve https / http://localhost:8080
This enables Funnel to forward HTTPS traffic from any path (using the root “/” mount-point) to the machine’s http://localhost:8080
.
Now for the magic! We’re one command away from being able to forward our webhook development server to this shared Funnel node, thus making it available over the Funnel we just set up.
From the other machines that you wish to share the Funnel with, start your development server for testing your webhooks. In the following example we use port 3000
as the local development server’s port. Finally, start an SSH reverse proxy connecting your local development server to the shared Funnel node, configured on its port 8080
, which we set up as our Funnel’s source, for example:
$ ssh -NT -R 8080:127.0.0.1:3000 github-hook-dev.corp-xyz.ts.net
You should be able to test your webhook or visit the URL https://github-hook-dev.corp-xyz.ts.net
and see requests going from the internet, through your shared Funnel node to your local development server!
Lastly, what if the shared Funnel is already in use? You’ll receive an error message when establishing the SSH reverse proxy. It will report a similar message to the following:
Warning: remote port forwarding failed for listen port 8080
Testing webhook receiver changes
Having a route accessible with Funnel means that other services on the internet can reach out to it and submit data, such as webhooks from vendors like GitHub or Stripe. If you are working on a webhook receiver (such as with go-playground/webhooks), you will want to test it with a faster turnaround time than if you deployed every change to the cloud.
You can use Funnel to expose the webhook receiver on your development machine and then use that URL for the service you are integrating with.
Assuming you have a local HTTP server running on port 3030
, enable serving port on your tailnet:
$ tailscale serve https / http://localhost:3030
Then enable Funnel:
$ tailscale funnel 443 on
Finally, get the public URL for your machine:
$ tailscale funnel status
https://example.pango-lin.ts.net (Funnel on)
Configure the other service to use your URL and webhook path as normal. You can trigger messages to your service like any other webhook receiver. As long as your development machine is turned on and connected to Tailscale, the webhooks will be routed to your development server with Funnel.
Use serve to bind local services to your tailnet
In addition to running an HTTPS server, you can use tailscale serve
to bind local TCP-based services to your Tailscale IP and make them available privately across your tailnet.
Here’s an example of rebinding your machine’s SSH server to port 2222
. You might find this helpful when using Tailscale SSH to provide backup access to your machine’s SSH server, for example:
$ tailscale serve tcp:2222 tcp://localhost:22
From another machine, connect as you normally would via SSH but add the port we configured as a flag to the command. For example:
$ ssh -p 2222 <user>@100.x.y.z