This guide sets up tsbridge on Debian as a dedicated system user, proxying multiple local services such as 127.0.0.1:3000 and 127.0.0.1:3333 into your Tailscale tailnet.
This setup does not use Docker discovery and does not require root for the running service.
- One
tsbridgeprocess for multiple services - Services stay bound to
127.0.0.1 - Access over Tailscale with HTTPS and MagicDNS names like:
https://app.<tailnet>.ts.nethttps://api.<tailnet>.ts.net
- A dedicated
tsbridgesystem user - A
systemdservice for automatic startup
- Debian with
systemd - Tailscale tailnet already exists
- Your local apps are already running on localhost
- Example backends:
127.0.0.1:3000127.0.0.1:3333127.0.0.1:8080
Install Go, then install tsbridge:
sudo apt update
sudo apt install -y golang-go
go install github.com/jtdowney/tsbridge/cmd/tsbridge@latest
sudo install -m 0755 "$HOME/go/bin/tsbridge" /usr/local/bin/tsbridge
/usr/local/bin/tsbridge -hIf you prefer, you can also use an upstream release binary instead of go install.
Create a locked-down service account with its own state directory:
sudo useradd --system --home /var/lib/tsbridge --shell /usr/sbin/nologin tsbridge
sudo mkdir -p /etc/tsbridge
sudo mkdir -p /var/lib/tsbridge
sudo chown tsbridge:tsbridge /var/lib/tsbridge
sudo chmod 0750 /var/lib/tsbridge
sudo chmod 0755 /etc/tsbridgeOpen the Tailscale admin console:
Create an OAuth client with:
- scope to generate auth keys
- the tag you want new tsnet devices to use, for example
tag:server
If you don't have a tag yet, create one here: https://login.tailscale.com/admin/acls/visual/tags
Save the values:
- OAuth client ID
- OAuth client secret
Important:
- The secret is shown once.
- The tag in your
tsbridgeconfig must match a tag the OAuth client is allowed to use.
Create the files:
sudo install -m 0600 -o tsbridge -g tsbridge /dev/null /etc/tsbridge/oauth-id
sudo install -m 0600 -o tsbridge -g tsbridge /dev/null /etc/tsbridge/oauth-secretEdit them:
sudo nano /etc/tsbridge/oauth-id
sudo nano /etc/tsbridge/oauth-secretPut only the raw values in the files:
/etc/tsbridge/oauth-id: your OAuth client ID/etc/tsbridge/oauth-secret: your OAuth client secret
Why tsbridge:tsbridge ownership?
Because the service runs as the tsbridge user. If you make these files 0600 root:root, tsbridge cannot read them.
Create /etc/tsbridge/tsbridge.toml:
[tailscale]
oauth_client_id_file = "/etc/tsbridge/oauth-id"
oauth_client_secret_file = "/etc/tsbridge/oauth-secret"
state_dir = "/var/lib/tsbridge"
default_tags = ["tag:server"]
[[services]]
name = "app"
backend_addr = "127.0.0.1:3000"
[[services]]
name = "api"
backend_addr = "127.0.0.1:3333"
[[services]]
name = "admin"
backend_addr = "127.0.0.1:8080"Set ownership and permissions:
sudo chown root:root /etc/tsbridge/tsbridge.toml
sudo chmod 0644 /etc/tsbridge/tsbridge.tomlNotes:
namebecomes the subdomain on your tailnet.backend_addrpoints to the local service.- Your backends can stay on
127.0.0.1.
Run a validation check as the service user:
sudo -u tsbridge /usr/local/bin/tsbridge -config /etc/tsbridge/tsbridge.toml -validateIf that passes, run a foreground test:
sudo -u tsbridge /usr/local/bin/tsbridge -config /etc/tsbridge/tsbridge.tomlFrom another device on your tailnet, test:
https://app.<tailnet>.ts.nethttps://api.<tailnet>.ts.nethttps://admin.<tailnet>.ts.net
Stop the foreground process with Ctrl+C once you confirm it starts correctly.
Create /etc/systemd/system/tsbridge.service:
[Unit]
Description=tsbridge Tailscale reverse proxy
After=network-online.target
Wants=network-online.target
[Service]
User=tsbridge
Group=tsbridge
ExecStart=/usr/local/bin/tsbridge -config /etc/tsbridge/tsbridge.toml
Restart=on-failure
RestartSec=5
WorkingDirectory=/var/lib/tsbridge
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/tsbridge
ReadOnlyPaths=/etc/tsbridge
[Install]
WantedBy=multi-user.targetReload systemd and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now tsbridge
sudo systemctl status tsbridgeWatch logs if needed:
sudo journalctl -u tsbridge -fWith the example config above, the service names map like this:
app->https://app.<tailnet>.ts.netapi->https://api.<tailnet>.ts.netadmin->https://admin.<tailnet>.ts.net
If MagicDNS is enabled in your tailnet, these names should resolve automatically.
Symptom:
tsbridgefails to start because it cannot readoauth-idoroauth-secret
Fix:
sudo chown tsbridge:tsbridge /etc/tsbridge/oauth-id /etc/tsbridge/oauth-secret
sudo chmod 0600 /etc/tsbridge/oauth-id /etc/tsbridge/oauth-secretCheck that the backend is actually listening:
ss -ltnp | rg '3000|3333|8080'Test locally on the server:
curl http://127.0.0.1:3000
curl http://127.0.0.1:3333
curl http://127.0.0.1:8080If default_tags = ["tag:server"] fails, your OAuth client may not be allowed to create devices with that tag.
Check:
- the tag selected on the OAuth client
- your tailnet tag ownership policy
Restart after config changes:
sudo systemctl restart tsbridgeView recent logs:
sudo journalctl -u tsbridge -n 100 --no-pagerCheck whether the service is enabled:
sudo systemctl is-enabled tsbridgetsbridgeis intended mainly for homelab and development use.- Keep OAuth secrets out of Git.
- Keep secrets readable only by the
tsbridgeuser. - Using a dedicated system user is better than running the service as
root. - If you later add Docker discovery, access to
/var/run/docker.sockis effectively high privilege.
/usr/local/bin/tsbridge
/etc/tsbridge/tsbridge.toml
/etc/tsbridge/oauth-id
/etc/tsbridge/oauth-secret
/var/lib/tsbridge/
/etc/systemd/system/tsbridge.service
https://github.com/jtdowney/tsbridgehttps://tailscale.com/docs/features/oauth-clientshttps://tailscale.com/docs/reference/trust-credentialshttps://tailscale.com/docs/reference/key-secret-management