Convention — tailnet service exposure (dedicated node per service)
Standing rule (user preference, 2026-08-08): every self-hosted web service gets its own dedicated Tailscale node + hostname (its own
tag:telepdevice), NOT a sharedtailscale serveport on thetelep-mainframenode. So a service lives athttps://<service>.taild4189d.ts.net, nothttps://telep-mainframe.taild4189d.ts.net:<port>.
Why
- Clean, memorable per-service hostnames (
pdf.taild4189d.ts.net,bambuddy.taild4189d.ts.net) instead of port-on-shared-host. - Independent ACLs / sharing / node lifecycle per service.
- Matches the existing knowledgebase and home-portal deployments.
Supersedes the older “simple same-node tailscale serve --https=<port>” pattern (frigate :443, top4 :8443, filestash :8445) for new services. Existing shared-serve services can stay as-is; new ones get their own node.
The pattern (second tailscaled per identity)
- Bind the app to loopback only (
127.0.0.1:<port>) — never exposed on the LAN. - Run a dedicated
tailscaledwith userspace networking, its own socket and state:
as a systemd unit/usr/sbin/tailscaled \ --tun=userspace-networking \ --statedir=/var/lib/tailscale-<service> \ --socket=/run/tailscale-<service>/tailscaled.sock \ --port=0tailscaled-<service>.service(model:tailscaled-knowledgebase.service,tailscaled-home.service). - Authenticate the node once (interactive login URL, or a reusable auth key):
tailscale --socket=/run/tailscale-<service>/tailscaled.sock up --hostname=<service> - Serve the loopback app as HTTPS on that node:
→tailscale --socket=/run/tailscale-<service>/tailscaled.sock serve --bg --https=443 http://127.0.0.1:<port>https://<service>.taild4189d.ts.net. - Add a tile to the home portal (
/home/levander/home-portal/index.html).
The gotcha that cost real time
Use
--statedir=<dir>, NOT--state=<file>. With--state=,serve --httpsfails with “no TailscaleVarRoot” and TLS returns000. Full write-up: 2026-07-24-knowledgebase §“The two that cost thought (separate-node + TLS)“.
Auth: always a TAGGED node (tag:telep)
Service nodes join tagged (tag:telep) — owned by the tailnet, non-expiring, ACL-governed — never as a personal-user device. The reliable way to tag a node is a reusable auth key minted WITH tag:telep at login.tailscale.com/admin/settings/keys (Tags: tag:telep), passed as --authkey=<key>.
tailscale --socket=/run/tailscale-<service>/tailscaled.sock up \
--hostname=<service> --authkey=<tag:telep key>
Let the tag come from the key
Do NOT also pass
--advertise-tags=tag:telep— that validates against the authenticating user’stagOwnersand can be rejected. The node inherits the tag from the key. (Same gotcha bit the babylon sidecar — see babylon-deploy-notes / gotcha #32.)
Observed 2026-08-11 —
--advertise-tagsdid work for theruviewnode
tailscale up --hostname=ruview --advertise-tags=tag:telepsucceeded and the node came uptags: ['tag:telep'],keyexpiry: None, owned by the tag — see 2026-08-11-ruview-esp32-csi-real-hardware. This does not overturn the rule above:--advertise-tagsvalidates against the authenticating user’stagOwners, and it passed only because that user ownstag:telep. The auth-key route is still preferred because it doesn’t depend on who authenticates.
Generating the key requires the Tailscale admin console (user action) — a subagent cannot mint it. Interactive tailscale up login would attach the node to a user, not a tag, so it’s not used for services.
Exception — host-network services (e.g. Bambuddy)
Services that must run network_mode: host (Bambuddy — SSDP discovery + virtual-printer ports) are awkward to put behind a userspace-networking dedicated node, because they bind real host ports and advertise the host’s tailnet IP for their virtual printers. For those: the web UI can still get its own node (serve the loopback UI port through a dedicated tailscaled), but any device-advertised IP (virtual printers) will use the host node’s IP. Decide per service; note the split in that service’s plan.
Related
- 2026-07-24-knowledgebase — the original dedicated-node +
--statedirpattern - 2026-07-24-global-dashboard — home portal (where tiles go)
- 2026-08-08-bambuddy-deploy-plan — the host-network exception case
- 2026-08-11-ruview-esp32-csi-real-hardware —
ruview.taild4189d.ts.net; a node that joined tagged via--advertise-tags(see the note above), and a reminder that a new hostname can hit HTTP 421 if the app validates theHostheader - telep-mainframe — host + the older shared-serve-port table