A Docker container that exposes local services to your Tailscale network. Combines Tailscale VPN, Tailscale Serve (HTTPS + TCP relays), and a Web UI for browser-based management.
- Web UI - Browser-based management on port 8021
- Automatic TLS - Tailscale Serve HTTPS relays with MagicDNS hostnames
- HTTPS Relays - Configure HTTPS reverse relays through the UI
- TCP Relays - Forward non-HTTP protocols through Tailscale Serve
- Backup & Restore - Save and restore configurations
- Dual Authentication - Token or Tailscale network authentication
- Multi-Platform - Docker images for amd64 and arm64
- Features
- Screenshots
- Why tailrelay?
- Technology Stack
- Quick Start
- Web UI
- Getting Started
- Development
- API Reference
- Troubleshooting
- Contributing
tailrelay provides secure remote access to self-hosted services:
- Secure Access: Tailscale's VPN eliminates port forwarding requirements
- Easy Configuration: Web UI handles setup without manual config files
- Automatic TLS: Tailscale Serve terminates TLS for HTTPS relays
- Protocol Support: HTTP/HTTPS proxies and TCP relays for any service
- Backup & Restore: Save and restore configurations
Useful for accessing Start9 services like BTCPayServer, LND, electrs, and Mempool without Tor.
| Component | Purpose | Documentation |
|---|---|---|
| Tailscale | VPN, MagicDNS, device authentication | Tailscale docs |
| Tailscale Serve | HTTPS reverse relays and TCP forwarding | Serve docs |
| Web UI | Browser-based management (Go backend, Svelte 5 + Tailwind CSS frontend) | See Web UI section |
# Pull the image
docker pull sudocarlos/tailrelay:latest
# Run the container
docker run -d --name tailrelay \
-v /path/to/data:/var/lib/tailscale \
-e TS_HOSTNAME=myserver \
-p 8021:8021 \
--net bridge \
sudocarlos/tailrelay:latest
# Access the Web UI and follow the Tailscale login link
open http://localhost:8021The Web UI provides browser-based management on port 8021. The frontend is a single-page application built with Svelte 5 (runes mode), Tailwind CSS v4, and Vite. All assets (JS, CSS, icons) are bundled locally -- no external CDN requests at runtime.
- Dashboard - Real-time Tailscale connection status and system health
- Tailscale Management - Connect/disconnect and view network peers
- HTTPS Relay Management - Add, edit, delete, and toggle HTTPS relays backed by
tailscale serve - TCP Relay Management - Add, edit, delete, and toggle TCP relays backed by
tailscale serve - Backup & Restore - Create and restore compressed tar.gz backups
- Live Log Viewer - Collapsible log console with SSE streaming and runtime log level control
- Dark Mode - System-aware theme toggle with localStorage persistence
- Keyboard Shortcuts -
n(new),r(refresh),b(backups),l(logs),t(theme)
The Web UI uses two authentication methods:
- Tailscale Network Authentication: Devices on your Tailscale network are automatically authenticated. If the container is not connected, the Web UI shows a Tailscale login link and polls until the device is connected.
- Token Authentication: A token is generated on first startup at
/var/lib/tailscale/.webui_tokenfor scripted access or legacy flows.
The Web UI runs on port 8021:
# Via Tailscale hostname (if HTTPS is enabled)
https://your-hostname.your-tailnet.ts.net:8021
# Or via local IP
http://localhost:8021- A Tailscale account with an active Tailnet (tailscale.com)
- HTTPS certificates enabled in Tailscale Admin console
- Docker or Podman installed
- Log into Tailscale Admin console and click DNS to enable MagicDNS.
- Tailnets created on or after October 20, 2022 have MagicDNS enabled by default.
- Review MagicDNS to understand how it works.
- Verify or set your Tailnet name
- Scroll down and enable HTTPS under HTTPS Certificates
tailrelay is available as a StartOS package via sudocarlos/tailrelay-startos.
Sideloading:
- Download the latest
tailrelay.s9pkfrom the tailrelay-startos releases page, or clone the repo and runmaketo build it yourself. - In the StartOS web UI menu, navigate to System → Sideload Service.
- Drag and drop or select the
tailrelay.s9pkfile to install. - Once installed, navigate to Services → Tailrelay and click Start.
For rapid iteration without rebuilding the full Docker image:
make frontend-build # Build Svelte SPA -> webui/cmd/webui/web/dist/
make dev-build # Build Go binary with embedded SPA + build metadataThis compiles ./data/tailrelay-webui with build metadata (version, commit, date) and embeds the SPA assets from web/dist/.
Frontend dev server:
cd webui/frontend
npm run dev # Starts Vite dev server with hot reloadDev asset override:
Set WEBUI_DEV_DIR to a directory containing a dist/ subdirectory (e.g., webui/cmd/webui/web) to serve assets from disk instead of the embedded files.
Manual build:
cd webui
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \
-ldflags="-w -s" \
-o ../data/tailrelay-webui ./cmd/webuiOption A: Mount Binary (Recommended)
Mount the local binary for instant updates:
# compose-test.yml
services:
tailrelay:
volumes:
- ./data/tailrelay-webui:/usr/bin/tailrelay-webui:ro
- ./tailscale/:/var/lib/tailscaleThen restart:
docker compose -f compose-test.yml restart tailrelayIteration workflow:
- Edit code in
webui/orwebui/frontend/ - Run
make frontend-build(if frontend changed) - Run
make dev-build - Restart container
- Test changes
Option B: Build Development Image
make dev-docker-buildThis builds a Docker image using the local binary.
# Development build with local binary
make frontend-build
make dev-build
make dev-docker-build
# Production build (multi-stage)
docker buildx build -t sudocarlos/tailrelay:latest .
# Show available targets
make helpThe test suite is split across three layers:
Covers internal/auth, internal/serve, internal/handlers, internal/backup,
and internal/web.
# From the repo root — uses the `make test` target
make test
# Or directly
cd webui && go test ./...
# Verbose output
cd webui && go test -v ./...Builds the full container image and smoke-tests container startup, process presence,
port availability, Tailscale health/metrics endpoints, and Web UI API. The suite lives in tests/integration/ and is driven by environment
variables.
# Setup (one-time)
cp .env.example .env
# Edit TAILRELAY_HOST and TAILNET_DOMAIN in .env
pip install pytest # one-time
# Run via Make
make integration-test
# Or directly
pytest tests/integration/ -vEnvironment variables (all have defaults; override in .env or shell):
| Variable | Default | Purpose |
|---|---|---|
TAILRELAY_HOST |
tailrelay-test |
Container hostname / Docker service name |
TAILNET_DOMAIN |
example.com |
Tailnet domain (used for HTTPS cert checks) |
COMPOSE_FILE |
compose-test.yml |
Compose file to spin up the stack |
BUILD_IMAGE |
1 |
Set to 0 to skip docker compose build |
IMAGE_TAG |
sudocarlos/tailrelay:dev |
Image to build/run |
STARTUP_WAIT |
8 |
Seconds to wait after container start |
GitHub Actions runs all three layers automatically on push/PR to main:
- frontend job:
npm install+npm run build - backend job:
go vet ./...+go test ./...+go build ./... - integration job: full Docker build +
pytest tests/integration/ -v
The dev-build target injects build information:
var (
version = "dev" // Git describe output
commit = "none" // Short commit hash
date = "unknown" // Build timestamp (UTC)
branch = "unknown" // Git branch
builtBy = "local" // System username
)Access these in webui/cmd/webui/main.go.
The Web UI backend exposes a JSON API on port 8021. All endpoints under /api/ require authentication except where noted. Authentication is via Tailscale network identity (100.x.y.z) or session cookie.
| Method | Path | Auth | Input | Description |
|---|---|---|---|---|
POST |
/api/tailscale/login |
No | -- | Initiate Tailscale login, returns auth URL |
GET |
/api/tailscale/poll |
No | -- | Poll login completion, sets session cookie |
GET |
/api/status |
Yes | -- | Aggregate system status |
GET |
/api/targets |
Yes | -- | List configured targets |
GET |
/api/tailscale/status |
Yes | -- | Tailscale status summary |
GET |
/api/tailscale/peers |
Yes | -- | Tailscale peer list |
POST |
/api/tailscale/logout |
Yes | -- | Deauthorize Tailscale node |
POST |
/api/tailscale/connect |
Yes | -- | Bring Tailscale up |
POST |
/api/tailscale/disconnect |
Yes | -- | Bring Tailscale down |
GET |
/api/serve/https/list |
Yes | -- | List all HTTPS relays |
GET |
/api/serve/https/get |
Yes | ?id= |
Get single HTTPS relay |
POST |
/api/serve/https/create |
Yes | JSON or multipart | Create HTTPS relay |
POST |
/api/serve/https/update |
Yes | JSON or multipart | Update HTTPS relay (id required) |
POST |
/api/serve/https/delete |
Yes | ?id= |
Delete HTTPS relay |
POST |
/api/serve/https/toggle |
Yes | JSON {id, enabled} |
Enable/disable HTTPS relay |
POST |
/api/serve/https/reconcile |
Yes | -- | Reconcile all HTTPS relays via tailscale serve |
GET |
/api/serve/tcp/list |
Yes | -- | List all TCP relays |
GET |
/api/serve/tcp/get |
Yes | ?id= |
Get single TCP relay |
POST |
/api/serve/tcp/create |
Yes | JSON | Create TCP relay |
POST |
/api/serve/tcp/update |
Yes | JSON | Update TCP relay (id required) |
POST |
/api/serve/tcp/delete |
Yes | ?id= |
Delete TCP relay |
POST |
/api/serve/tcp/toggle |
Yes | JSON {id, enabled} |
Enable/disable TCP relay |
POST |
/api/serve/tcp/start |
Yes | ?id= |
Start TCP relay |
POST |
/api/serve/tcp/stop |
Yes | ?id= |
Stop TCP relay |
POST |
/api/serve/tcp/restart |
Yes | ?id= |
Restart TCP relay |
POST |
/api/serve/tcp/reconcile |
Yes | -- | Reconcile all enabled TCP relays |
GET |
/api/backup/list |
Yes | -- | List backups with metadata |
POST |
/api/backup/create |
Yes | JSON {backup_type} |
Create backup (full or config-only) |
POST |
/api/backup/restore |
Yes | JSON {filename} |
Restore from backup |
POST |
/api/backup/delete |
Yes | ?filename= |
Delete backup |
GET |
/api/backup/download |
Yes | ?filename= |
Download backup (.tar.gz) |
POST |
/api/backup/upload |
Yes | multipart backup |
Upload backup (max 32 MB) |
GET |
/api/logs |
Yes | -- | Historical logs + current level |
GET |
/api/logs/stream |
Yes | -- | SSE live log stream |
GET |
/api/logs/level |
Yes | -- | Get current log level |
POST |
/api/logs/level |
Yes | JSON {level} |
Set log level (debug, info, warn, error) |
{
"id": "abc123",
"hostname": "myservice",
"port": 8080,
"target": "192.168.1.10:3000",
"tls": true,
"trusted_proxies": false,
"host_header": "",
"enabled": true,
"autostart": true,
"running": true,
"tls_error": ""
}{
"id": "a1b2c3d4e5f6",
"listen_port": 9000,
"target_host": "192.168.1.10",
"target_port": 3000,
"enabled": true,
"autostart": true
}The GET /api/serve/tcp/list response wraps each relay in {"Relay": {...}, "Running": true}.
{
"filename": "tailrelay-backup-20260307-120000.tar.gz",
"size": 102400,
"timestamp": "2026-03-07T12:00:00Z",
"metadata": {
"timestamp": "2026-03-07T12:00:00Z",
"version": "v0.7.0",
"hostname": "my-node",
"backup_type": "full"
}
}All endpoints return errors as:
{
"status": "error",
"message": "Description of what went wrong"
}Check container status:
docker ps | grep tailrelayVerify port mapping:
docker port tailrelayCheck logs:
docker logs tailrelay | grep -i webuiVerify listening port:
docker exec tailrelay netstat -tulnp | grep 8021Retrieve token:
docker exec tailrelay cat /var/lib/tailscale/.webui_tokenEnsure you're accessing from Tailscale network or clear browser cache.
Check current serve status:
docker exec tailrelay tailscale serve statusForce reconcile from saved UI configuration:
curl -X POST http://localhost:8021/api/serve/https/reconcile
# or for TCP relays:
curl -X POST http://localhost:8021/api/serve/tcp/reconcileTest target connectivity:
docker exec tailrelay nc -zv target-host target-portContributions welcome:
- Issues: GitHub Issues
- Pull Requests: GitHub PRs
- Documentation: Help improve docs or add examples
# Clone repository
git clone https://github.com/sudocarlos/tailrelay.git
cd tailrelay
# Build locally
docker build -t tailrelay:dev .
# Run tests
docker compose -f compose-test.yml up -dSee Development section for WebUI development workflow.
See CHANGELOG.md for the full release history.
The Web UI supports light and dark themes and is fully responsive on mobile.
| Light — Desktop | Dark — Desktop |
|---|---|
![]() |
![]() |
| Light — Mobile | Dark — Mobile |
![]() |
![]() |
| Light — Desktop | Dark — Desktop |
|---|---|
![]() |
![]() |
| Light — Mobile | Dark — Mobile |
![]() |
![]() |
Log console expanded (dark):
Mobile navigation menu open:
![]() |
![]() |
| Light — Desktop | Dark — Desktop |
|---|---|
![]() |
![]() |
| Light — Mobile | Dark — Mobile |
![]() |
![]() |
| Light — Desktop | Dark — Desktop |
|---|---|
![]() |
![]() |
Full-page (dark):
| Light — Desktop | Dark — Desktop |
|---|---|
![]() |
![]() |
Mobile (dark):
Open source project. See repository for license details.
- Tailscale - VPN platform and
tailscale serve - Start9 - Inspiration for this project
- Original project by @hollie
Last full review completed at commit 89eabb1. To check what has changed since:
git log --oneline 89eabb1..HEAD -- README.md webui/internal/web/server.go



















