ts-plug wraps your local server and exposes it on your tailnet with automatic TLS and DNS.
ts-plug is a reverse proxy that:
- Starts and manages your upstream server process
- Connects to your tailnet
- Provides automatic HTTPS with valid TLS certificates
- Optionally exposes services publicly via Tailscale Funnel
- Supports HTTP, HTTPS, and DNS proxying
Build from source:
make ts-plugInstall to $GOPATH/bin:
make installThe basic pattern is:
ts-plug [flags] -- [your-server-command]Everything after -- is treated as the command to run.
Run a Python HTTP server on your tailnet:
ts-plug -hostname myserver -- python -m http.server 8080Run a Node.js app:
ts-plug -hostname api -- node server.jsRun a Go server:
ts-plug -hostname webapp -- go run main.go┌─────────────────────────────────────────────────────────┐
│ Your Local Machine │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ts-plug │ starts │ Your Server │ │
│ │ │ ──────> │ localhost:80 │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
└─────────┼───────────────────────────────────────────────┘
│ Tailscale (encrypted)
│
┌─────────┼───────────────────────────────────────────────┐
│ Your tailnet │
│ │ │
│ │ ┌──────────────┐ ┌──────────────┐ │
│ └──> │ HTTPS:443 │───>│ Team Members │ │
│ │ (with TLS) │ │ Devices │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
ts-plug:
- Starts your upstream server process
- Connects to your tailnet
- Provisions TLS certificates automatically
- Listens for connections on your tailnet
- Reverse proxies traffic to your local server
- Command after
--- The server command to execute
-
-hostname/-hn- Hostname on your tailnet (default: "tsmultiplug")ts-plug -hostname myapp -- python app.py # Access at: https://myapp.tailnet-name.ts.net -
-dir- Directory to store Tailscale state (default: ".data")ts-plug -dir /var/lib/tsplug -hostname api -- ./server
By default, ts-plug enables HTTPS on port 443 proxying to localhost:8080.
-
-http- Enable HTTP listener (default port mapping: 80:8080)# Enable HTTP, proxy port 80 to localhost:8080 ts-plug -http -hostname web -- python -m http.server 8080 -
-http-port- Customize HTTP port mapping# Listen on port 8000, proxy to localhost:3000 ts-plug -http-port 8000:3000 -hostname web -- node server.js # Listen and proxy both on port 9000 ts-plug -http-port 9000 -hostname web -- ./server
-
-https- Enable HTTPS listener (default port mapping: 443:8080)ts-plug -https -hostname secure -- python -m http.server 8080
-
-https-port- Customize HTTPS port mapping# Listen on port 8443, proxy to localhost:3000 ts-plug -https-port 8443:3000 -hostname web -- node server.js
-
-dns- Enable DNS listener (default port mapping: 53:53)ts-plug -dns -hostname dns -- pihole-FTL
-
-dns-port- Customize DNS port mapping# Forward DNS from port 53 to localhost:5353 ts-plug -dns-port 53:5353 -hostname resolver -- dnsmasq
-
-public- Enable Tailscale Funnel for public HTTPS accessts-plug -public -hostname demo -- python -m http.server 8080 # Now accessible from the public internet!This is perfect for:
- Webhook testing
- Demo sites
- Temporary public APIs
- Sharing work with clients
-
-log- Set log level (debug, info, warn, error)ts-plug -log debug -hostname myapp -- node server.js
-
-debug-tsnet- Enable verbose tsnet.Server loggingts-plug -debug-tsnet -hostname myapp -- ./server
Enable multiple protocols simultaneously:
# HTTP, HTTPS, and DNS
ts-plug -http -https -dns -hostname multi -- ./serverMap different ports for each protocol:
ts-plug \
-http-port 80:3000 \
-https-port 443:3000 \
-hostname myapp \
-- node server.jsYour server can detect when it's running under ts-plug:
if [ "$TSPLUG_ACTIVE" = "1" ]; then
echo "Running behind ts-plug!"
fiimport os
if os.getenv('TSPLUG_ACTIVE') == '1':
print("Running behind ts-plug!")ts-plug automatically provisions valid TLS certificates for your tailnet hostname. No configuration needed.
When requests come through ts-plug, these headers are added:
Tailscale-User-Login- User's login emailTailscale-User-Name- User's display nameTailscale-User-Profile-Pic- URL to user's profile picture
Your server can use these for authentication:
@app.route('/api/whoami')
def whoami():
return {
'login': request.headers.get('Tailscale-User-Login'),
'name': request.headers.get('Tailscale-User-Name'),
'picture': request.headers.get('Tailscale-User-Profile-Pic')
}Services are only accessible to devices on your tailnet (unless -public is used).
Share your dev server with teammates:
ts-plug -hostname dev-alice -- npm run dev
# Tell your teammate to visit: https://dev-alice.tailnet.ts.netTest webhooks without ngrok:
ts-plug -public -hostname webhook-test -- python webhook_server.py
# Use the public URL in GitHub/Stripe/etc webhook settingsUse as an entrypoint to eliminate sidecar containers:
COPY ts-plug /usr/local/bin/
ENTRYPOINT ["ts-plug", "-hostname", "myapp", "--"]
CMD ["python", "app.py"]See docker.md for detailed examples.
Run Pi-hole with both DNS and HTTP:
ts-plug \
-dns \
-http \
-hostname pihole \
-- pihole-FTLIf you get "address already in use", another process is listening on the configured port:
# Check what's using port 8080
lsof -i :8080
# Use a different port
ts-plug -https-port 443:3000 -hostname myapp -- node server.jsIf ts-plug can't connect to your server:
- Verify your server is listening on the correct port
- Make sure it's listening on
0.0.0.0or127.0.0.1, not a specific IP - Check logs with
-log debug
First run will prompt you to authenticate with Tailscale:
ts-plug -hostname test -- python -m http.server 8080
# Follow the URL to authenticateState is saved in the -dir location (default: .data/)
ts-plug -hostname nextjs-dev -https-port 443:3000 -- npm run devts-plug -hostname django -https-port 443:8000 -- python manage.py runserverts-plug -public -hostname my-site -- python -m http.server 8080ts-plug -hostname api-v1 -https-port 443:5000 -- flask run| Feature | ts-plug | ts-unplug |
|---|---|---|
| Direction | Local → tailnet | tailnet → Local |
| Use Case | Share local services | Access remote services |
| Starts Process | Yes | No |
| TLS | Automatic | Proxies existing |
| Public Access | Optional | No |
- ts-unplug Guide - Access remote services locally
- Use Cases - Real-world patterns
- Docker Examples - Container integration
- Main README - Quick start guide