AgentSkillsCN

tailserve

指导 Claude Code 通过 Tailscale 预览 Web 项目——支持直接访问 Tailnet,利用 `tailscale serve` 实现多项目 HTTPS 路由,借助 `tailscale funnel` 进行公开分享。同时涵盖各框架特有的命令行工具。

SKILL.md
--- frontmatter
name: tailserve
description: Guides Claude Code on previewing web projects over Tailscale — direct tailnet access, `tailscale serve` for multi-project HTTPS routing, and `tailscale funnel` for public sharing. Includes framework-specific commands.

Tailserve: Web Project Previewing over Tailscale

Core Concepts

There are three ways to access dev servers over Tailscale. Choose based on your needs:

1. Direct Tailnet Access (simplest)

  • Server binds to 0.0.0.0, access via http://<hostname>:<port>
  • Best for: single project, quick iteration
  • Limitation: HTTP only, one project per port, must remember ports

2. tailscale serve (recommended for multiple projects)

  • Server binds to localhost (default), Tailscale proxies it with automatic HTTPS
  • Best for: running multiple projects, clean URLs, HTTPS
  • Access via: https://<hostname>.<tailnet>.ts.net/path

3. tailscale funnel (public sharing)

  • Extends tailscale serve to the public internet
  • Best for: sharing with people outside your tailnet
  • Access via: same HTTPS URL, but publicly accessible

Getting the Tailscale Hostname

bash
tailscale status --self | awk 'NR==1 {print $2}'

For direct access, the preview URL is: http://<tailscale-hostname>:<port> For serve/funnel, the URL is: https://<hostname>.<tailnet>.ts.net

Quick Reference Table

FrameworkBind to 0.0.0.0Default Port
Vitenpm run dev -- --host 0.0.0.05173
Next.jsnpm run dev -- -H 0.0.0.03000
Wranglernpx wrangler dev --ip 0.0.0.08787
Astronpm run dev -- --host 0.0.0.04321
Djangopython manage.py runserver 0.0.0.0:80008000
Flaskapp.run(host='0.0.0.0', port=5000)5000
Expressapp.listen(3000, '0.0.0.0')3000
Staticpython3 -m http.server 8000 --bind 0.0.0.08000

Note: The 0.0.0.0 bind is only needed for direct tailnet access. When using tailscale serve, servers can stay on localhost.

Framework-Specific Commands

Vite (Vue, React, Svelte, etc.)

bash
npm run dev -- --host 0.0.0.0

Config alternative (vite.config.js/ts):

javascript
export default defineConfig({
  server: {
    host: '0.0.0.0',
    allowedHosts: ['your-tailscale-hostname'],
  },
});

Important: Vite 6+ blocks requests from unrecognized hosts by default. When using direct tailnet access, add the Tailscale hostname to server.allowedHosts. This is not needed when using tailscale serve (requests come from localhost).

Default port: 5173

Next.js

bash
npm run dev -- -H 0.0.0.0

Config alternative (package.json):

json
"dev": "next dev -H 0.0.0.0"

Default port: 3000

Create React App (CRA)

bash
HOST=0.0.0.0 npm start

Config alternative (.env):

code
HOST=0.0.0.0

Default port: 3000

Cloudflare Workers (wrangler)

bash
npx wrangler dev --ip 0.0.0.0

Config alternative (wrangler.toml):

toml
[dev]
ip = "0.0.0.0"

Default port: 8787

Astro

bash
npm run dev -- --host 0.0.0.0

Config alternative (astro.config.mjs):

javascript
export default defineConfig({
  server: { host: '0.0.0.0' },
  vite: {
    server: {
      allowedHosts: ['your-tailscale-hostname'],
    },
  },
});

Important: Astro uses Vite under the hood. Vite 6+ blocks unrecognized hosts, so add allowedHosts when using direct tailnet access. Not needed with tailscale serve.

Default port: 4321

Python Flask

python
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Default port: 5000

Python Django

bash
python manage.py runserver 0.0.0.0:8000

Default port: 8000

Node.js/Express

javascript
app.listen(3000, '0.0.0.0', () => {
  console.log('Server running on 0.0.0.0:3000');
});

Default port: 3000

Static File Servers

bash
# Python
python3 -m http.server 8000 --bind 0.0.0.0

# npx serve
npx serve -l 3000 --listen 0.0.0.0

# http-server
npx http-server -a 0.0.0.0 -p 8080

Multi-Project Access with tailscale serve

tailscale serve creates a reverse proxy from your Tailscale HTTPS hostname to local dev servers. This is the recommended approach when running multiple projects.

Basic usage

bash
# Proxy your Tailscale HTTPS URL to a local dev server
tailscale serve http://localhost:3000

This makes https://<hostname>.<tailnet>.ts.net proxy to localhost:3000.

Key insight: Because tailscale serve proxies from the Tailscale daemon, the dev server does NOT need to bind to 0.0.0.0. Normal localhost binding works fine.

Path-based routing

Run multiple projects and access each on a different path:

bash
# Project A on /app (Next.js on port 3000)
tailscale serve --set-path /app http://localhost:3000

# Project B on /docs (Vite on port 5173)
tailscale serve --set-path /docs http://localhost:5173

# Project C on /api (Express on port 8080)
tailscale serve --set-path /api http://localhost:8080

Access them at:

  • https://<hostname>.<tailnet>.ts.net/app
  • https://<hostname>.<tailnet>.ts.net/docs
  • https://<hostname>.<tailnet>.ts.net/api

Background mode

bash
# Run in background (persists across reboots)
tailscale serve --bg http://localhost:3000

Managing serve config

bash
# Show current serve configuration
tailscale serve status

# Remove a specific path
tailscale serve --remove /app

# Reset all serve configuration
tailscale serve reset

Public Sharing with Tailscale Funnel

Funnel extends tailscale serve to make your server publicly accessible on the internet.

Basic usage

bash
# Make a local server publicly accessible
tailscale funnel http://localhost:3000

This creates a public URL: https://<hostname>.<tailnet>.ts.net

With path routing

bash
# Share only a specific path publicly
tailscale funnel --set-path /demo http://localhost:3000

Stopping funnel

bash
# Stop funnel
tailscale funnel off

# Or reset all serve/funnel config
tailscale serve reset

Troubleshooting

"Connection Refused" or "Can't Connect" (direct access)

Problem: Server is binding to localhost instead of 0.0.0.0.

Check: Look at server startup logs:

  • http://localhost:3000 or http://127.0.0.1:3000 -> wrong for direct access
  • http://0.0.0.0:3000 or shows network interfaces -> correct

Fix: Add the --host 0.0.0.0 flag (or equivalent) for your framework. Or switch to tailscale serve which doesn't require this.

Vite "Blocked host" error

Problem: Vite 6+ blocks requests from unrecognized hostnames.

Fix: Add the Tailscale hostname to allowedHosts in vite.config.js/ts:

javascript
server: {
  allowedHosts: ['your-tailscale-hostname'],
}

This is only needed for direct tailnet access. tailscale serve proxies through localhost, so Vite sees the request as coming from a recognized host.

Firewall issues

macOS may prompt to allow incoming connections the first time a server binds to 0.0.0.0. Click "Allow".

Wrong hostname

bash
# Check current hostname
tailscale status --self

# Set a custom hostname
sudo tailscale up --hostname=my-mac

tailscale serve not working

  1. Make sure the dev server is actually running on the port you specified
  2. Check serve status: tailscale serve status
  3. Ensure you're accessing via the full https://<hostname>.<tailnet>.ts.net URL
  4. Try resetting: tailscale serve reset and re-adding

Guidelines for Claude Code

When helping users preview web projects over Tailscale, use this decision tree:

  1. Single project, quick test? -> Direct access: bind to 0.0.0.0, use http://<hostname>:<port>
  2. Multiple projects or want HTTPS? -> tailscale serve: keep localhost binding, use path-based routing
  3. Need to share externally? -> tailscale funnel: extends serve to the public internet

For any approach:

  • Always get the Tailscale hostname via tailscale status --self
  • Always provide the complete preview URL
  • Vite/Astro (direct access only): add allowedHosts to the Vite config
  • Remind users that tailscale serve doesn't require 0.0.0.0 binding