Real-world scenarios for using ts-plug and ts-unplug together.
- Development Workflows
- Testing Scenarios
- Deployment Patterns
- Team Collaboration
- Hybrid Cloud Architectures
Scenario: You're developing a web app locally but want to use a shared staging database.
Solution: Use ts-unplug to bring the remote database to localhost:
# Terminal 1: Make remote database available locally
ts-unplug -dir ./state-db -port 5432 postgres-staging.tailnet.ts.net:5432
# Terminal 2: Run your app normally
DATABASE_URL=postgresql://localhost:5432/mydb npm run dev
# Terminal 3: Share your dev instance with teammates
ts-plug -hostname dev-yourname -https-port 443:3000 -- npm run devBenefits:
- Real data without database dumps
- Team can test your work instantly
- No VPN or complex networking
Scenario: You're working on one microservice that depends on several others.
Solution: Use ts-unplug for dependencies, ts-plug to share your service:
# Expose remote auth service locally
ts-unplug -dir ./state-auth -port 8001 auth-service.tailnet.ts.net &
# Expose remote payment service locally
ts-unplug -dir ./state-payment -port 8002 payment-service.tailnet.ts.net &
# Run your service with local env vars
export AUTH_URL=http://localhost:8001
export PAYMENT_URL=http://localhost:8002
go run main.go &
# Share your development service
ts-plug -hostname orders-dev -- go run main.goScenario: Testing a mobile app that needs to hit your local backend.
Solution: Use ts-plug to expose your backend with a stable URL:
# Start backend with ts-plug
ts-plug -hostname mobile-api-dev -- npm run dev
# Configure mobile app to use:
# https://mobile-api-dev.tailnet.ts.net
# Mobile device must be on your Tailnet (install Tailscale app)Benefits:
- No need to update URLs constantly
- Works from physical devices
- Proper HTTPS for realistic testing
Scenario: Testing GitHub/Stripe/Twilio webhooks locally.
Solution: Use ts-plug with -public flag:
# Start your webhook handler
ts-plug -public -hostname webhook-test -- python webhook_server.py
# Copy the public URL and paste into webhook settings
# https://webhook-test.tailnet.ts.net
# Webhooks will hit your local serverBenefits:
- No third-party tunneling services
- Automatic TLS certificates
- Tailscale identity in headers
Scenario: Running E2E tests against a staging API.
Solution: Use ts-unplug to make staging API appear local:
# Make staging API available at localhost
ts-unplug -dir ./state -port 8080 api-staging.tailnet.ts.net
# Run tests pointing to localhost
API_URL=http://localhost:8080 npm run test:e2eBenefits:
- No need to change test configuration
- Faster than VPN
- Can run in CI with Tailscale
Scenario: Load test a service on your Tailnet.
Solution: Use ts-unplug to proxy, run local load testing tools:
# Proxy remote service
ts-unplug -dir ./state -port 8080 service-under-test.tailnet.ts.net
# Run load tests against localhost
ab -n 10000 -c 100 http://localhost:8080/api/endpointScenario: Deploy containerized apps without Tailscale sidecar complexity.
Solution: Use ts-plug as the container entrypoint:
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Add ts-plug binary
COPY --from=ghcr.io/yourorg/ts-plug:latest /ts-plug /usr/local/bin/
# Use ts-plug as entrypoint
ENTRYPOINT ["ts-plug", "-hostname", "myapp", "-dir", "/var/lib/tsplug", "--"]
CMD ["npm", "start"]Docker Compose:
version: '3'
services:
app:
build: .
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
volumes:
- tsplug-state:/var/lib/tsplug
volumes:
tsplug-state:Benefits:
- No separate sidecar container
- Simpler orchestration
- Automatic HTTPS
Scenario: Expose homelab services securely.
Solution: Use ts-plug for each service:
# Pi-hole
ts-plug -dns -http -hostname pihole -- pihole-FTL
# Jellyfin media server
ts-plug -hostname jellyfin -https-port 443:8096 -- jellyfin
# Home Assistant
ts-plug -hostname homeassistant -https-port 443:8123 -- hassBenefits:
- No port forwarding
- Automatic HTTPS
- No dynamic DNS needed
Scenario: Share a demo with a client without deployment.
Solution: Use ts-plug with -public:
# Start demo environment
ts-plug -public -hostname demo-acme-corp -- npm start
# Share URL with client (no Tailscale required)
# https://demo-acme-corp.tailnet.ts.netScenario: Reviewer wants to test a branch without checking it out.
Solution: Developer shares their branch with ts-plug:
# Developer runs their branch
ts-plug -hostname feature-xyz-alice -- npm run dev
# Reviewer accesses in browser
# https://feature-xyz-alice.tailnet.ts.net
# No git checkout needed!Scenario: Designers need to preview work-in-progress features.
Solution: Each developer exposes their environment:
# Frontend developer
ts-plug -hostname frontend-bob -https-port 443:3000 -- npm run dev
# Backend developer uses ts-unplug to access Bob's frontend
ts-unplug -dir ./state -port 3000 frontend-bob.tailnet.ts.net:443
# Backend developer exposes API
ts-plug -hostname api-carol -- go run main.goScenario: Remote pair programming with live server access.
Solution: Host shares their development environment:
# Host runs server
ts-plug -hostname pairing-session -- bundle exec rails server
# Participant accesses same server
# Both see changes in real-timeScenario: Develop locally but use cloud-hosted database.
Solution:
# Put database on Tailscale (could be RDS with TS subnet router)
# Access it locally
ts-unplug -dir ./state -port 5432 rds-proxy.tailnet.ts.net:5432
# Run app locally
DATABASE_URL=postgresql://localhost:5432/prod npm run devScenario: Services spread across AWS, GCP, on-prem.
Solution: Use Tailscale subnet routers and ts-unplug:
# Access AWS service
ts-unplug -dir ./state-aws -port 8001 aws-api.tailnet.ts.net &
# Access GCP service
ts-unplug -dir ./state-gcp -port 8002 gcp-api.tailnet.ts.net &
# Access on-prem service
ts-unplug -dir ./state-onprem -port 8003 onprem-api.tailnet.ts.net &
# Your app sees everything as localhost
export AWS_API=http://localhost:8001
export GCP_API=http://localhost:8002
export ONPREM_API=http://localhost:8003
./run-app.shScenario: Deploy to edge locations (Raspberry Pi, IoT devices).
Solution: Run ts-plug on edge devices:
# On Raspberry Pi
ts-plug -hostname sensor-living-room -- python sensor.py
# On another Pi
ts-plug -hostname sensor-garage -- python sensor.py
# Access all sensors from central dashboard
# No complex networking, no public IPsScenario: You need to route local traffic through a proxy that's only accessible on a different tailnet.
For example:
- Your local device is on tailnet A
- The proxy server is on tailnet B
- You want your local app to use the proxy without direct access
Solution: Use ts-unplug to expose the remote proxy locally, then configure your app to use localhost as the proxy:
# Terminal 1: Make the remote proxy available locally
ts-unplug -dir ./state-proxy -port 8888 proxy.tailnet-b.ts.net:3128
# Terminal 2: Configure your app to use the local proxy
export HTTP_PROXY=http://localhost:8888
export HTTPS_PROXY=http://localhost:8888
# Run your application - all traffic now goes through the remote proxy
curl https://api.example.com
# This request routes: app → localhost:8888 → tailnet-b proxy → internetAdvanced example with multiple proxies:
# Corporate proxy for work traffic
ts-unplug -dir ./state-corp-proxy -port 8888 corp-proxy.work-tailnet.ts.net:3128 &
# Research proxy for academic traffic
ts-unplug -dir ./state-research-proxy -port 8889 proxy.university-tailnet.ts.net:8080 &
# Use different proxies for different apps
HTTP_PROXY=http://localhost:8888 curl https://internal.corp.com # Uses corporate proxy
HTTP_PROXY=http://localhost:8889 curl https://research.edu # Uses research proxyDocker container example:
# Start proxy tunnel
ts-unplug -dir ./state -port 3128 proxy.other-tailnet.ts.net:3128
# Run container using host's proxy
docker run --network host \
-e HTTP_PROXY=http://localhost:3128 \
-e HTTPS_PROXY=http://localhost:3128 \
myappBenefits:
- Access proxies on different tailnets without complex routing
- No need to expose proxy publicly
- Can use organization-specific proxies from any device
- Works with apps that only support HTTP proxy environment variables
Use ts-plug/ts-unplug as a lightweight service mesh:
# Each service exposes itself with ts-plug
ts-plug -hostname service-a -- ./service-a
ts-plug -hostname service-b -- ./service-b
ts-plug -hostname service-c -- ./service-c
# Services discover each other via Tailscale DNS
curl https://service-b.tailnet.ts.net/apiBenefits:
- Built-in encryption
- Automatic service discovery
- Identity-based access
- No complex mesh configuration
Scenario: Migrating from monolith to microservices.
Old monolith:
ts-plug -hostname legacy-monolith -- ./monolithNew microservices consume monolith:
# New service uses ts-unplug to access old monolith
ts-unplug -dir ./state -port 8080 legacy-monolith.tailnet.ts.net
# New service exposes itself
ts-plug -hostname new-service -- ./new-serviceFrontend can use both during migration:
// Old endpoints
fetch('https://legacy-monolith.tailnet.ts.net/api/users')
// New endpoints
fetch('https://new-service.tailnet.ts.net/api/users')Scenario: Complex development setup script.
dev-env.sh:
#!/bin/bash
# Start remote services locally
ts-unplug -dir ./state-db -port 5432 postgres.tailnet.ts.net:5432 &
ts-unplug -dir ./state-redis -port 6379 redis.tailnet.ts.net:6379 &
ts-unplug -dir ./state-auth -port 8001 auth.tailnet.ts.net &
# Wait for proxies to start
sleep 2
# Start local services and expose them
ts-plug -hostname api-dev -- npm run dev:api &
ts-plug -hostname web-dev -https-port 443:3000 -- npm run dev:web &
echo "Development environment ready!"
echo "API: https://api-dev.tailnet.ts.net"
echo "Web: https://web-dev.tailnet.ts.net"Leverage Tailscale's built-in identity:
// In your service
func handler(w http.ResponseWriter, r *http.Request) {
user := r.Header.Get("Tailscale-User-Login")
if user == "" {
http.Error(w, "Unauthorized", 401)
return
}
// User is authenticated by Tailscale
// Implement authorization based on user
if !isAuthorized(user) {
http.Error(w, "Forbidden", 403)
return
}
// Handle request
}Share access temporarily without changing firewall rules:
# Start service with ts-plug
ts-plug -public -hostname temp-demo -- python -m http.server 8080
# Share URL with external user
# When done, stop ts-plug
# Access automatically revoked