ts-unplug is a reverse HTTP proxy that exposes a Tailscale service to localhost.
ts-unplug allows you to:
- Access remote Tailscale services as if they were local
- Develop against remote APIs without changing code
- Test against staging environments seamlessly
- Use tools that only support localhost URLs
Think of it as the reverse of ts-plug: instead of exposing local to remote, it exposes remote to local.
Build from source:
make ts-unplugInstall to $GOPATH/bin:
make installts-unplug -dir <state-dir> [options] <remote-addr>Access a Tailscale service locally:
ts-unplug -dir ./state -port 8080 myserver.tailnet.ts.net
# Now access at http://localhost:8080Connect to a specific port on a remote service:
ts-unplug -dir ./state -port 3000 database.tailnet.ts.net:5432
# PostgreSQL now available at localhost:3000Access a staging API as localhost:
ts-unplug -dir ./state -port 8080 api-staging.tailnet.ts.net
# Your app can now use http://localhost:8080 for API calls-
<remote-addr>- Remote Tailscale address (hostname or hostname:port)- If no port specified, defaults to port 80
- Examples:
myserver,myserver.tailnet.ts.net,myserver:8080
-
-dir- Directory for tsnet server state (required)- Stores Tailscale authentication and connection state
- Example:
-dir ./state,-dir /var/lib/tsunplug
-
-port- Local port to listen on (default: 80)ts-unplug -dir ./state -port 8080 remote-api.tailnet.ts.net
-
-hostname- Hostname for the tsnet server (default: "tsunplug")ts-unplug -dir ./state -hostname myproxy -port 3000 remote.tailnet.ts.net
-
-debug-tsnet- Enable verbose tsnet.Server loggingts-unplug -dir ./state -port 8080 -debug-tsnet remote.tailnet.ts.net
Develop locally while using a remote database:
# Start the proxy
ts-unplug -dir ./state -port 5432 postgres.tailnet.ts.net:5432
# In another terminal, run your app pointing to localhost
DATABASE_URL=postgresql://localhost:5432/mydb npm run devTest your frontend against a staging API:
ts-unplug -dir ./state -port 8080 api-staging.tailnet.ts.net
# Update your .env.local
echo "NEXT_PUBLIC_API_URL=http://localhost:8080" > .env.local
npm run devMany tools only work with localhost URLs. Use ts-unplug to bridge the gap:
# Access a remote admin panel locally
ts-unplug -dir ./state -port 8080 admin.tailnet.ts.net
# Now use curl, Postman, etc. with localhost
curl http://localhost:8080/api/statusRun multiple instances to access different services:
# Terminal 1: Database
ts-unplug -dir ./state-db -port 5432 postgres.tailnet.ts.net:5432
# Terminal 2: Redis
ts-unplug -dir ./state-redis -port 6379 redis.tailnet.ts.net:6379
# Terminal 3: API
ts-unplug -dir ./state-api -port 8080 api.tailnet.ts.netNote: Each instance needs its own -dir to avoid conflicts.
Debug a remote service with local tools:
ts-unplug -dir ./state -port 8080 buggy-service.tailnet.ts.net
# Use your favorite debugging tools
curl -v http://localhost:8080/debug
http localhost:8080/health # HTTPie┌─────────────────────────────────────────────────────────┐
│ Your Local Machine │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Your App │ HTTP │ ts-unplug │ │
│ │ localhost:80 │ ──────> │ │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
└───────────────────────────────────┼─────────────────────┘
│ Tailscale
│ (encrypted)
┌───────────────────────────────────┼─────────────────────┐
│ Remote Tailscale Network │ │
│ │ │
│ ┌────────▼───────┐ │
│ │ Remote Service │ │
│ │ myserver:80 │ │
│ └────────────────┘ │
└─────────────────────────────────────────────────────────┘
ts-unplug:
- Connects to your tailnet
- Establishes a connection to the remote service
- Listens on localhost
- Forwards all traffic through the encrypted Tailscale connection
The remote and local ports don't need to match:
# Remote service on :8080, local access on :3000
ts-unplug -dir ./state -port 3000 api.tailnet.ts.net:8080Run as a background service:
# Using systemd (example)
cat > /etc/systemd/system/ts-unplug-api.service <<EOF
[Unit]
Description=ts-unplug proxy for API
After=network.target
[Service]
Type=simple
User=youruser
ExecStart=/usr/local/bin/ts-unplug -dir /var/lib/tsunplug -port 8080 api.tailnet.ts.net
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now ts-unplug-apiAccess a service from inside a Docker container:
# Start ts-unplug on host
ts-unplug -dir ./state -port 8080 api.tailnet.ts.net
# Run container with access to host network
docker run --network host myapp
# Container can now access http://localhost:8080Or use host.docker.internal:
docker run -e API_URL=http://host.docker.internal:8080 myappts-unplug inherits your Tailscale authentication. The remote service sees requests as coming from your Tailscale identity.
The -dir contains sensitive Tailscale credentials. Protect it appropriately:
chmod 700 ./statets-unplug only listens on localhost (127.0.0.1), not on all network interfaces. This means only processes on your local machine can access it.
You must specify a state directory:
# Wrong
ts-unplug myserver.tailnet.ts.net
# Right
ts-unplug -dir ./state myserver.tailnet.ts.netProvide the remote address as a positional argument:
ts-unplug -dir ./state -port 8080 myserver.tailnet.ts.netIf you can't connect to localhost:
- Verify ts-unplug is running and shows "HTTP proxy listening"
- Check you're using the correct local port
- Verify the remote service is accessible from your Tailnet
If ts-unplug starts but can't connect to the remote:
- Verify the remote hostname is correct
- Check the remote service is running
- Ensure you have access to the remote service on your Tailnet
- Try accessing the service directly:
tailscale ping myserver.tailnet.ts.net
If the local port is already taken:
# Check what's using the port
lsof -i :8080
# Use a different port
ts-unplug -dir ./state -port 8081 myserver.tailnet.ts.netts-unplug -dir ./state -port 5432 postgres.tailnet.ts.net:5432
# Connect with psql
psql -h localhost -p 5432 -U myuser mydbts-unplug -dir ./state -port 6379 redis.tailnet.ts.net:6379
# Use redis-cli
redis-cli -h localhost -p 6379ts-unplug -dir ./state -port 8080 api-staging.tailnet.ts.net
# Test with curl
curl http://localhost:8080/api/usersts-unplug -dir ./state -port 3000 admin.tailnet.ts.net:3000
# Open in browser
open http://localhost:3000| 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-plug Guide - Expose local services to Tailnet
- Use Cases - Real-world patterns
- Main README - Quick start guide