Funnel & Serve¶
Overview¶
Tailscale Serve exposes local services to your tailnet. Tailscale Funnel exposes services to the public internet.
┌──────────────────────────────────────────────────────────────────────────────┐
│ Serve vs Funnel │
│ │
│ Tailscale Serve Tailscale Funnel │
│ ───────────────── ──────────────── │
│ │
│ Local Service Local Service │
│ │ │ │
│ ▼ ▼ │
│ tailscale serve tailscale funnel │
│ │ │ │
│ ▼ ▼ │
│ Your Tailnet Public Internet │
│ (private) (anyone can access) │
│ │
│ Access: my-server.tailnet.ts.net Access: my-server.tailnet.ts.net │
│ (Tailscale devices only) (anyone with URL) │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Tailscale Serve¶
Basic Usage¶
Expose a local port to your tailnet:
# Serve local port 3000
tailscale serve 3000
# Serve with HTTPS (automatic cert)
tailscale serve https / http://localhost:3000
Serve Commands¶
# Start serving
tailscale serve [flags] {port|path|url}
# Show current configuration
tailscale serve status
# Stop serving
tailscale serve off
# Reset all serve config
tailscale serve reset
Examples¶
# Serve a web app
tailscale serve 8080
# Serve static files
tailscale serve / /var/www/html
# Serve with path prefix
tailscale serve /api http://localhost:3000
# Serve TCP directly
tailscale serve tcp:5432 tcp://localhost:5432
# Serve multiple paths
tailscale serve /app http://localhost:3000
tailscale serve /api http://localhost:4000
HTTPS Certificates¶
Tailscale automatically provisions TLS certificates:
# Serve with HTTPS
tailscale serve https / http://localhost:3000
# Access at:
# https://my-server.tailnet.ts.net
Certificates are: - Automatically provisioned via Let's Encrypt - Automatically renewed - Valid for your Tailscale hostname
Accessing Served Content¶
# From any device on your tailnet
curl https://my-server.tailnet.ts.net
# Or use MagicDNS short name
curl https://my-server
Tailscale Funnel¶
Funnel exposes services to the public internet.
Public Access
Funnel makes your service accessible to anyone on the internet. Use with caution.
Enable Funnel¶
# In admin console:
# DNS → Enable HTTPS Certificates
# Also enable Funnel in settings
# Then on device:
tailscale funnel 443
Funnel Commands¶
# Start funnel
tailscale funnel {port|target}
# Show status
tailscale funnel status
# Stop funnel
tailscale funnel off
# Reset configuration
tailscale funnel reset
Examples¶
# Funnel port 443 to local 3000
tailscale funnel 443
# Funnel with specific backend
tailscale funnel https / http://localhost:3000
# Funnel TCP (e.g., for SSH)
tailscale funnel tcp:22
Accessing Funneled Services¶
This URL is accessible from anywhere on the internet.
Configuration File¶
For complex setups, use a configuration file:
// serve.json
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"my-server.tailnet.ts.net:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:3000"
},
"/api": {
"Proxy": "http://127.0.0.1:4000"
},
"/static": {
"Path": "/var/www/static"
}
}
}
},
"AllowFunnel": {
"my-server.tailnet.ts.net:443": true
}
}
Apply configuration:
Configuration Options¶
| Field | Description |
|---|---|
TCP | TCP port configuration |
Web | HTTP/HTTPS handlers |
Handlers | Path-based routing |
Proxy | Reverse proxy to local URL |
Path | Serve static files |
AllowFunnel | Enable public access |
Use Cases¶
Development Preview¶
Share a development server with teammates:
# Local dev server
npm run dev # Running on localhost:3000
# Share via Tailscale
tailscale serve 3000
# Teammates access:
# https://dev-laptop.tailnet.ts.net
Webhook Receiver¶
Expose a webhook endpoint publicly:
# Local webhook handler
python webhook_server.py # Port 8000
# Expose via Funnel
tailscale funnel https /webhooks http://localhost:8000
# Give external service:
# https://my-server.tailnet.ts.net/webhooks
Self-Hosted Services¶
Expose Nextcloud, Gitea, etc.:
# Nextcloud on port 80
tailscale serve https / http://localhost:80
# With Funnel for public access
tailscale funnel 443
Quick File Sharing¶
Serve a directory temporarily:
Docker Integration¶
Sidecar Pattern¶
# docker-compose.yml
version: "3.8"
services:
tailscale:
image: tailscale/tailscale:latest
hostname: my-app
cap_add:
- NET_ADMIN
- NET_RAW
volumes:
- /dev/net/tun:/dev/net/tun
- ts-state:/var/lib/tailscale
- ./serve.json:/config/serve.json
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve.json
webapp:
image: nginx:alpine
network_mode: service:tailscale
volumes:
ts-state:
// serve.json
{
"TCP": {"443": {"HTTPS": true}},
"Web": {
"my-app.tailnet.ts.net:443": {
"Handlers": {
"/": {"Proxy": "http://127.0.0.1:80"}
}
}
}
}
Security Considerations¶
Serve (Tailnet Only)¶
- Only accessible by authenticated tailnet members
- Protected by Tailscale ACLs
- No public exposure
Funnel (Public)¶
- Accessible by anyone with the URL
- No authentication by default
- Consider:
- Adding authentication to your app
- Rate limiting
- Web Application Firewall
- Monitoring access logs
ACL for Serve¶
Troubleshooting¶
Serve Not Working¶
# Check serve status
tailscale serve status
# Check Tailscale is connected
tailscale status
# Verify local service is running
curl localhost:3000
# Check for port conflicts
ss -tlnp | grep :443
HTTPS Certificate Issues¶
# Verify HTTPS is enabled in admin console
# DNS → Enable HTTPS Certificates
# Check certificate status
tailscale cert my-server.tailnet.ts.net
# Manual cert fetch
tailscale cert --cert-file=cert.pem --key-file=key.pem my-server.tailnet.ts.net
Funnel Not Accessible¶
# Check Funnel is enabled in admin console
# Settings → Enable Funnel
# Verify funnel status
tailscale funnel status
# Test locally first via serve
tailscale serve status
Connection Refused¶
# Verify local service is listening
curl -v localhost:3000
# Check firewall isn't blocking
sudo iptables -L -n | grep 3000
# Ensure serve config points to correct port
tailscale serve status
Best Practices¶
- Start with Serve before enabling Funnel
- Add authentication for Funnel-exposed services
- Monitor access to public endpoints
- Use HTTPS always
- Review ACLs for serve access control
- Limit Funnel scope to necessary paths only