How to Expose Local Apps Securely with Tailscale Serve and Funnel (Free HTTPS in Minutes)

If you build or demo web apps, you have probably fought with port forwarding, firewalls, or complicated reverse proxies. Tailscale’s Serve and Funnel features make this painless: Serve exposes your local service to your tailnet (only authenticated users in your Tailscale network), and Funnel optionally publishes it to the public internet with automatic HTTPS. This guide shows a step-by-step setup in minutes on Linux, with tips for macOS and Windows.

What you will set up

You will run a local app (for example, on http://localhost:3000) and expose it securely via Tailscale in two modes:

Tailnet-only (Serve): Only people signed into your tailnet can access it (ideal for internal testing).
Public (Funnel): Anyone on the internet can access a public HTTPS URL that Tailscale provisions for you (great for demos, webhooks, or sharing temporary previews).

Prerequisites

- A Tailscale account (free is fine for personal use).
- Tailscale installed and logged in on the computer that runs your app (Linux/macOS/Windows).
- Ability to turn on the “Funnel” feature in the Tailscale admin console (if you plan to go public).
- A local service running (e.g., a dev server on port 3000).

1) Install and log in to Tailscale

On Ubuntu/Debian, the quickest way is:

curl -fsSL https://tailscale.com/install.sh | sh

Then bring the node online and sign in:

sudo tailscale up

Verify you are connected:

tailscale status

On macOS and Windows, install the client from the Tailscale website, sign in, and ensure the device appears in your admin console.

2) Expose a local service to your tailnet (Serve)

Let’s assume your app runs at http://localhost:3000. Use Tailscale Serve to proxy HTTPS traffic from your device’s Tailscale HTTPS endpoint to your local port:

tailscale serve https / proxy http://localhost:3000

That command maps path / to your local app. Check the status of your Serve configuration:

tailscale serve status

Now open the tailnet URL printed by the command (usually something like https://<device-name>.<tailnet>.ts.net/). Only users/authenticated devices in your tailnet can access it. This is perfect for internal reviews without exposing anything publicly.

3) Make it public with HTTPS (Funnel)

To publish your service to the public internet, first enable the Funnel feature in the Tailscale admin console (Settings → Feature preview/Settings → Funnel, depending on your account). You can restrict which devices and ports are allowed to use Funnel.

Once enabled, turn on Funnel for HTTPS (port 443) on the device:

tailscale funnel 443 on

That’s it. Tailscale will automatically provision a valid TLS certificate and a public URL (again, typically https://<device-name>.<tailnet>.ts.net/). Share this link with anyone; they do not need Tailscale to view your app.

To turn Funnel off again:

tailscale funnel 443 off

4) Useful variations

- Serve a different path: tailscale serve https /app proxy http://localhost:5173
- Serve a TCP port to your tailnet (e.g., Postgres): tailscale serve tcp 5432 127.0.0.1:5432
- Reset all Serve mappings on this device: tailscale serve reset

5) Testing and verification checklist

- Local works: Browse http://localhost:3000.
- Tailnet works: From another device on your tailnet, open https://<device-name>.<tailnet>.ts.net/. You should see a valid HTTPS certificate and your app.
- Public works: After turning on Funnel, test from a device not logged into Tailscale (cellphone on LTE, for example). The same URL should load over HTTPS.

6) Security tips

- Prefer tailnet-only Serve during development. Switch on Funnel only when you actually need a public demo or webhook endpoint.
- If your app needs authentication, keep it enabled. Funnel does not add login by default; it only provides HTTPS and routing.
- Avoid exposing admin panels, databases, or file shares via Funnel. Keep those tailnet-only or behind application-level authentication.

7) Troubleshooting

“403: Funnel not allowed” — Ensure the Funnel feature is enabled for your tailnet in the admin console and that your device/port is permitted.
Port conflicts — If something else is bound to 443 on your device, stop it or remap your Serve path (e.g., Serve at /app) and still use Funnel on 443.
Blank page or mixed content — If your app hardcodes http:// asset URLs, fix them to be relative or https://.
Service not reachable — Confirm your local app responds at the target URL (e.g., curl http://localhost:3000). Then run tailscale serve status to confirm the mapping exists.

8) Keeping it tidy

- List current mappings: tailscale serve status
- Remove a specific route: change your mapping command or reset and re-apply.
- Keep Tailscale updated: use your OS package manager or reinstall via the install script periodically. Check your version with tailscale version.

Why this approach is great

You get automatic TLS, end-to-end encrypted transport, and a clean URL without touching DNS, firewalls, or port forwards. For internal stakeholders, Serve keeps traffic private to your tailnet. For public demos, Funnel gives you one command to go live, then one command to go dark. It’s a powerful quality-of-life upgrade for anyone who ships or supports web apps.

Comments

Popular Posts

Install Ollama and Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install Ollama + Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install a Local AI Chatbot on Ubuntu 24.04 with Ollama and Open WebUI (Step-by-Step)

Trending Now

Recovering from Btrfs Boot Failures Using GUI Tools on Fedora

By the end of this guide the reader will be able to identify a Btrfs‑based Fedora installation, boot from a live USB, list and restore snapshots using the graphical utilities btrfs‑assistant and snapper, and verify that the system returns to a functional state without resorting to the command line. Understanding the Btrfs Layout Used by Fedora Fedora Workstation and Fedora KDE install the root filesystem as a single Btrfs partition that contains two default sub‑volumes. One sub‑volume holds the traditional “/” hierarchy, while the second is dedicated to /var/lib/machines . The latter exists to keep container images out of snapshot operations; it remains empty on systems that do not run virtual machines. Because Btrfs stores data in sub‑volumes rather than separate partitions, a snapshot captures the state of an entire sub‑volume at a point in time. The installer (Anaconda) automatically registers these sub‑volumes with the snapper service. Snapper maintains a series of read‑only ...