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 viahttp://<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 serveto the public internet - •Best for: sharing with people outside your tailnet
- •Access via: same HTTPS URL, but publicly accessible
Getting the Tailscale Hostname
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
| Framework | Bind to 0.0.0.0 | Default Port |
|---|---|---|
| Vite | npm run dev -- --host 0.0.0.0 | 5173 |
| Next.js | npm run dev -- -H 0.0.0.0 | 3000 |
| Wrangler | npx wrangler dev --ip 0.0.0.0 | 8787 |
| Astro | npm run dev -- --host 0.0.0.0 | 4321 |
| Django | python manage.py runserver 0.0.0.0:8000 | 8000 |
| Flask | app.run(host='0.0.0.0', port=5000) | 5000 |
| Express | app.listen(3000, '0.0.0.0') | 3000 |
| Static | python3 -m http.server 8000 --bind 0.0.0.0 | 8000 |
Note: The
0.0.0.0bind is only needed for direct tailnet access. When usingtailscale serve, servers can stay on localhost.
Framework-Specific Commands
Vite (Vue, React, Svelte, etc.)
npm run dev -- --host 0.0.0.0
Config alternative (vite.config.js/ts):
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
npm run dev -- -H 0.0.0.0
Config alternative (package.json):
"dev": "next dev -H 0.0.0.0"
Default port: 3000
Create React App (CRA)
HOST=0.0.0.0 npm start
Config alternative (.env):
HOST=0.0.0.0
Default port: 3000
Cloudflare Workers (wrangler)
npx wrangler dev --ip 0.0.0.0
Config alternative (wrangler.toml):
[dev] ip = "0.0.0.0"
Default port: 8787
Astro
npm run dev -- --host 0.0.0.0
Config alternative (astro.config.mjs):
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
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Default port: 5000
Python Django
python manage.py runserver 0.0.0.0:8000
Default port: 8000
Node.js/Express
app.listen(3000, '0.0.0.0', () => {
console.log('Server running on 0.0.0.0:3000');
});
Default port: 3000
Static File Servers
# 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
# 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:
# 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
# Run in background (persists across reboots) tailscale serve --bg http://localhost:3000
Managing serve config
# 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
# Make a local server publicly accessible tailscale funnel http://localhost:3000
This creates a public URL: https://<hostname>.<tailnet>.ts.net
With path routing
# Share only a specific path publicly tailscale funnel --set-path /demo http://localhost:3000
Stopping funnel
# 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:3000orhttp://127.0.0.1:3000-> wrong for direct access - •
http://0.0.0.0:3000or 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:
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
# Check current hostname tailscale status --self # Set a custom hostname sudo tailscale up --hostname=my-mac
tailscale serve not working
- •Make sure the dev server is actually running on the port you specified
- •Check serve status:
tailscale serve status - •Ensure you're accessing via the full
https://<hostname>.<tailnet>.ts.netURL - •Try resetting:
tailscale serve resetand re-adding
Guidelines for Claude Code
When helping users preview web projects over Tailscale, use this decision tree:
- •Single project, quick test? -> Direct access: bind to
0.0.0.0, usehttp://<hostname>:<port> - •Multiple projects or want HTTPS? ->
tailscale serve: keep localhost binding, use path-based routing - •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
allowedHoststo the Vite config - •Remind users that
tailscale servedoesn't require0.0.0.0binding