Tailscale gives CI-Hub a private, encrypted way to reach the Hub and installed apps from anywhere without opening inbound ports to the public internet. It is the recommended option when you want simple remote access for operators, support staff, or a small trusted team.
The hub-tailscale sidecar is on by default on every stack (dev, prod, Tauri desktop, docker-only). Compose starts it when COMPOSE_PROFILES includes private-vpn (the desktop app and CLI add this automatically once the sidecar has an auth key or a saved login). To turn the sidecar or the Hub's Tailscale Serve entries off, see Turn Private VPN off. Legacy PRIVATE_VPN_ENABLED is not used for gating.
If the host already has Tailscale installed and the Hub can reach tailscaled.sock, CI-Hub can use the host client instead of the sidecar.
- Private remote access to the Hub UI and apps without public DNS or port forwarding
- Encrypted connections between approved devices on your tailnet
- Simple onboarding from the CI-Hub onboarding flow or Settings → Network
- Per-app remote URLs when apps are exposed with the
tailscaleexposure mode
Use this option when you set up Tailscale from the Hub UI.
- Start CI-Hub with the default
private-vpnprofile enabled. - Open the Hub onboarding flow, or go to Settings → Network later.
- In the Tailscale section, select Log In with Tailscale.
- Complete the sign-in flow in the browser window that opens.
- Return to CI-Hub and confirm the Tailscale status shows as connected.
After connection, the UI shows the Hub's Tailscale IP address and hostname. Save that hostname for remote administration.
This is useful for pre-provisioned devices or admin-managed deployments.
- Generate a Tailscale auth key in the Tailscale admin console.
- Add it to the Hub environment as
TAILSCALE_AUTHKEY. - Start or restart the Hub.
- Confirm the device appears in your tailnet and that Settings → Network shows the connection as active.
CI-Hub also accepts the legacy variable
HEADSCALE_PREAUTH_KEY, but new deployments should useTAILSCALE_AUTHKEY.
- Disable: two switches in the hub
.envturn off the sidecar and the Hub's Tailscale Serve entries separately. See Turn Private VPN off. - Auth: set
TAILSCALE_AUTHKEYfor unattended setup, or use browser sign-in from the Hub UI. - Routes: by default
HUB_TAILSCALE_EXTRA_ARGStargets the public Tailscale control plane and advertises172.18.0.0/16so tailnet clients can reach the Hub's Docker bridge. Change this if your Docker network uses a different CIDR. - Sidecar name: CI-Hub uses
TAILSCALE_SIDECAR_CONTAINERwhen it needs to rundocker exec ... tailscale ...against a non-default container name.
The backend TailscaleService talks to Tailscale via docker exec hub-tailscale tailscale … when the Hub container has no host Tailscale socket.
Pooling inference capacity across multiple CI-Hub devices on the same tailnet is a separate, opt-in feature built on top of this connection — see
hub-pool.md.
Once connected, look in Settings → Network for the hostname shown by Tailscale.
While the VPN is connected, CI-Hub publishes its own dashboard on the tailnet via Tailscale Serve:
https://<device-name>.<tailnet>.ts.net/
This is also the origin the memory-connect / login ceremony uses for callers arriving over the VPN, so it must stay published for those flows to work. Publishing requires MagicDNS and HTTPS Certificates to be enabled for the tailnet (Tailscale admin console → DNS) — the same requirement as per-app Serve.
When CI-Hub uses a host Tailscale client instead of the sidecar (the host
has a reachable tailscaled.sock), the dashboard is additionally reachable on
the Hub API port directly (http://<device-name>:5002/). With the default
sidecar deployment that direct-port form does NOT work — the sidecar is a
separate container and forwards nothing on its own; only the Serve entries
above are published.
With a host Tailscale client, the Hub writes Serve config through the host's
tailscaled, which accepts writes only from root or the host's Tailscale
operator. If the Hub runs as an ordinary account, the Hub logs one warning with
the command that makes that account the operator, and skips publishing until you
run it on the host. Settings → Network shows the same reason and command,
with a copy button, and the access card of each unpublished Private VPN app says
that Tailscale refused the change instead of Pending. Both clear once
tailscaled accepts the Hub's next change. The account differs between hosts
(uid 1000 on most appliances, 1001 on some), so use the command from the
warning, or find the account with
ps -o user= -p "$(docker inspect -f '{{.State.Pid}}' ci-hub)" and
run sudo tailscale set --operator=<account>.
The Hub checks that the entry answers for the device's current name. If you
rename the device in the Tailscale admin console, the next exposure sync (every
five minutes) publishes the Hub under the new name. The old name's entry stays in
the Serve config, but nothing resolves that name any more. If
TAILSCALE_SERVE_USER_DISABLED=true is set, the Hub does not republish itself; it
logs one warning with the tailscale serve command to run instead.
Subnet routes caveat: the sidecar advertises
172.18.0.0/16(the Hub's Docker bridge) as a fallback path. Linux clients do not accept subnet routes by default (--accept-routes), the route must be approved in the admin console, and any client that itself runs Docker typically owns the same subnet locally — accepting the route there would capture that client's own Docker traffic. Prefer the Serve URLs; treat the subnet route as opt-in for clients that understand the trade-off.
When you install or update an app and choose Tailscale as the exposure mode, CI-Hub keeps Tailscale Serve in sync for that app.
CI-Hub publishes each app on a dedicated HTTPS port on the Hub node:
https://<device-name>.<tailnet>.ts.net:<app-port>/
For example, an app assigned port 8138 is available at
https://hub-demo.example.ts.net:8138/. Open the generated URL shown by CI-Hub
and test it from another device signed in to the same tailnet.
When an app stops, CI-Hub removes the Serve entry it published for that app. It
removes only entries it published, which it records in
state/tailscale-serve-ownership.json under its data directory. If you run your
own tailscale serve on the same host, CI-Hub leaves that entry in place unless
it uses port 443, which the Hub keeps for its dashboard, or the port of a running
app with Tailscale exposure. If TAILSCALE_SERVE_USER_DISABLED=true is set,
CI-Hub neither publishes nor removes any Serve entry.
Two switches in the hub .env turn off different parts of Private VPN. Set either
one to true and restart the stack.
| Variable | What it turns off | Read by |
|---|---|---|
PRIVATE_VPN_USER_DISABLED |
The hub-tailscale sidecar: the CLI leaves the private-vpn profile out of COMPOSE_PROFILES. The Hub still publishes itself and its Private VPN apps through the host's Tailscale client, if the host has one. |
The CLI. The desktop app ignores it and removes the line on its next start, because earlier versions of the desktop app wrote it on their own. |
TAILSCALE_SERVE_USER_DISABLED |
Tailscale Serve: the Hub never writes Serve config. It publishes neither its dashboard nor any app, removes nothing it published before, and stops offering Private VPN as an app's access option. | The Hub. The desktop app keeps it across restarts. |
Use PRIVATE_VPN_USER_DISABLED on a host that runs its own Tailscale client and
does not need the sidecar. Use TAILSCALE_SERVE_USER_DISABLED when you manage
the host's Serve config yourself.
Recommended operator workflow:
- Install or open Tailscale on your laptop, phone, or support workstation.
- Join the same tailnet as the CI-Hub device.
- In CI-Hub, verify Settings → Network shows Tailscale as active.
- Use the Hub hostname to open the admin UI remotely.
- For an app that should stay private, select Tailscale as its exposure mode during install or update.
- Share the resulting Tailscale URL only with tailnet members who should have access.
This workflow is ideal for remote maintenance, operator access, and private demos without exposing the Hub publicly through Cloudflare Tunnel.
- Prefer Tailscale for private admin access and use Cloudflare Tunnel only when you need internet-facing sharing.
- Use auth keys created for the correct environment and rotate them when staff or devices change.
- Treat
TAILSCALE_AUTHKEYlike a secret. Do not paste it into screenshots, tickets, or chat messages. - Review which users and devices are approved in the Tailscale admin console.
- Keep the advertised route in
HUB_TAILSCALE_EXTRA_ARGSlimited to the networks CI-Hub actually needs to expose. - If you use the host Tailscale client instead of the sidecar, make sure the Hub only has access to the socket intentionally.
| Problem | What to check |
|---|---|
| Tailscale section says not available | Make sure the private-vpn profile is running, or install Tailscale on the host and mount the daemon socket into the Hub container. |
| Browser login opens but CI-Hub never shows connected | Wait a few seconds for the status refresh, then revisit Settings → Network. Confirm the device appears in the Tailscale admin console. |
| Auth key login fails immediately | Verify the key starts with tskey-auth- and is still valid in the Tailscale admin console. |
| Remote device cannot reach the Hub or apps | Confirm the remote device is logged in to the same tailnet and that the advertised route in HUB_TAILSCALE_EXTRA_ARGS matches the Docker network used by the Hub. |
Logs still mention headscale:8080 after upgrading |
Reconnect Tailscale once so the device state is rewritten against controlplane.tailscale.com. If you override HUB_TAILSCALE_EXTRA_ARGS, keep an explicit --login-server=https://controlplane.tailscale.com unless you intentionally run your own control plane. |
| App URL works locally but not via Tailscale | Confirm the Hub and client are on the same tailnet, approve the HTTPS/Serve consent link shown by Hub if needed, then re-save the app with Tailscale exposure mode. |
Logs say serve config denied |
The Hub uses the host Tailscale client and is neither root nor the host's Tailscale operator. Run the sudo tailscale set --operator=… command from the warning once on the host; the next sync publishes the Hub. |
| Peers fail TLS to the Hub after a device rename | Wait for the next exposure sync, which runs every five minutes. If peers still fail after that, check the Hub logs for serve config denied, or, if TAILSCALE_SERVE_USER_DISABLED=true is set, for Tailscale Serve has no :443 entry and run the command it names. |
| Need to turn Tailscale off temporarily | Set TAILSCALE_SERVE_USER_DISABLED=true in the hub .env and restart the stack. The Hub then stops publishing to Tailscale Serve but leaves existing entries in place, because other Hubs may reach it through them. It also stops repairing its own entry after a device rename or a tailscale serve reset, and only logs a warning, so pool peers lose this Hub until someone publishes it by hand. To remove the Hub's entry, run sudo tailscale serve --https=443 off on the host. To stop the sidecar as well, also set PRIVATE_VPN_USER_DISABLED=true. |
Private VPN app stays Pending, and the log says Private VPN is turned off for this Hub (PRIVATE_VPN_USER_DISABLED=true) |
Hub versions starting with 0.2.73 read the sidecar switch as "never write Tailscale Serve config", and the desktop app set that switch on its own. Update the Hub. A Hub that should stay off Tailscale Serve needs TAILSCALE_SERVE_USER_DISABLED=true instead. |
Use this short checklist during onboarding or handoff:
- Device is connected to the correct tailnet
- Settings → Network shows Tailscale as active
- Hub hostname has been tested from a second device
- Any private apps that need remote access are set to Tailscale exposure mode
- Auth keys are stored securely and rotated when needed