# Tailscale Funnel — Permanent Public URLs for Local Services

Expose a local service (e.g., a static website on `127.0.0.1:8888`) to the public internet with a **stable, permanent URL** via Tailscale Funnel. Unlike Cloudflare Quick Tunnels (`trycloudflare.com`), Funnel URLs are persistent — they survive reboots, don't change, and don't expire.

## Prerequisites

- Tailscale running on the server (check: `docker ps --filter "name=tailscale"`)
- Tailscale container must use `network_mode: host` (to reach `127.0.0.1:<PORT>`)
- Local service must be running and reachable at `127.0.0.1:<PORT>`
- Tailscale account owner must enable Funnel once per node via admin console

## Check Current State

```bash
# Is Tailscale running?
docker ps --filter "name=tailscale" --format "{{.Names}} {{.Status}}"

# Network mode (must be "host")
docker inspect tailscale --format '{{.HostConfig.NetworkMode}}'

# Current serve/funnel config
docker exec tailscale tailscale serve status 2>&1
docker exec tailscale tailscale funnel status 2>&1
# "No serve config" → not yet configured
```

## Setup Procedure

### 1. Configure Tailscale Serve

```bash
docker exec tailscale sh -c 'tailscale serve --bg --set-path / http://127.0.0.1:<PORT> 2>&1'
```

If Funnel is not yet enabled for this node, the command will output an admin URL:
```
https://login.tailscale.com/f/serve?node=<nodeID>
```

**The user must click this link** to enable Funnel in the Tailscale admin console. This is a one-time manual step — the agent cannot automate it.

### 2. Enable Funnel (after user clicks admin link)

```bash
# Re-run serve (should succeed now)
docker exec tailscale sh -c 'tailscale serve --bg --set-path / http://127.0.0.1:<PORT> 2>&1'

# Enable Funnel
docker exec tailscale sh -c 'tailscale funnel --bg --set-path / http://127.0.0.1:<PORT> 2>&1'
```

### 3. Verify

```bash
docker exec tailscale tailscale funnel status 2>&1
# Should show active funnel config

docker exec tailscale tailscale serve status 2>&1
# Should show active serve config
```

## Result

The service is available at:
```
https://<tailnet-name>.ts.net/<path>
```

Example: `https://zimaos.tailnet-name.ts.net/veritas-website.html`

This URL is **permanent** — it survives reboots, doesn't change, and doesn't expire.

## Pitfalls

1. **Funnel enablement is manual.** The admin console link must be clicked by the Tailscale account owner. The agent cannot automate this step. Present the link to the user and wait for confirmation.

2. **`tailscale serve` times out waiting for admin action.** If Funnel is not yet enabled, the command will hang waiting for the user to click the admin link. Use `timeout=15` and expect exit code 124. The output will still contain the admin URL — extract it and present to the user.

3. **Harmless DOCKER_CONFIG warning.** Every `docker exec tailscale` command outputs:
   ```
   WARNING: Error loading config file: open /DATA/.docker/config.json: permission denied
   ```
   This is harmless noise from the Docker CLI inside the container. Ignore it.

4. **`tailscale status --json` may fail.** The `--json` flag requires a recent Tailscale version. If it fails with `JSONDecodeError`, fall back to plain `tailscale status` and parse the output manually.

5. **Serve must be configured before Funnel.** `tailscale funnel` only works after `tailscale serve` is active. Don't skip the serve step.

6. **Port must be bound to 127.0.0.1, not 0.0.0.0.** Tailscale Funnel connects to localhost. If the service binds to `0.0.0.0`, it still works but `127.0.0.1` is the canonical target.

## Comparison: Funnel vs Quick Tunnel

| Feature | Tailscale Funnel | Cloudflare Quick Tunnel |
|---------|-----------------|------------------------|
| URL stability | Permanent, never changes | Random subdomain, changes every restart |
| Uptime guarantee | Yes (as long as server is up) | No guarantee, dies after hours/days |
| Account needed | Existing Tailscale account | None |
| Setup complexity | One-time admin click + 2 CLI commands | Download binary + 1 CLI command |
| Best for | Permanent public exposure | Temporary demos, quick sharing |
