Upstream docs: https://tailscale.com/docs/
Everything not listed in this document should behave the same as upstream Tailscale. If a feature, setting, or behavior is not mentioned here, the upstream documentation is accurate and fully applicable.
Tailscale is a private WireGuard®
mesh network that connects your devices securely without port forwarding, static
IPs, or complex firewall rules. Running Tailscale on StartOS joins your StartOS
server to your tailnet, exposes the Tailscale device web interface, and lets you
expose other StartOS services to your tailnet via tailscale serve, or to the public internet via tailscale funnel.
- Image and Container Runtime
- Volume and Data Layout
- Installation and First-Run Flow
- Configuration Management
- Network Access and Interfaces
- Actions (StartOS UI)
- Backups and Restore
- Health Checks
- Dependencies
- Limitations and Differences
- What Is Unchanged from Upstream
- Contributing
- Quick Reference for AI Consumers
| Property | Value |
|---|---|
| Image | ghcr.io/tailscale/tailscale:v1.104.4 |
| Architectures | x86_64, aarch64 |
| Runtime | Two daemons in one subcontainer |
Two daemons share a single subcontainer (with sharedRun: true so that action
temp containers can reach the tailscaled Unix socket):
tailscaled— the Tailscale daemon, running in userspace networking modetailscale-web— runstailscale web --listen=0.0.0.0:8080, exposing the management UI
tailscale-web starts only after tailscaled is ready.
| Volume | Mount Point | Purpose |
|---|---|---|
tailscale |
/var/lib/tailscale |
Persistent daemon state and identity |
startos |
(JS runtime) | Serve port mappings (store.json), cached Tailscale IP/DNS (status.json) |
Key files:
tailscale/tailscaled.state— node identity, keys, and tailnet membershipstartos/store.json— top-level object containingmachineName(the Tailscale hostname),authKey(nullable string, persisted between restarts and cleared once the node is logged in),controlServer(nullable string, a custom control server URL such as Headscale;nullmeans the default Tailscale control plane), andserves(maps{ packageId: { interfaceId: { port, hostId, scheme, internalPort } } }for all configured serves)startos/status.json— cached{ ip, dnsName }rewritten whenever the connected node reports new values
All state persists across restarts. The node retains its Tailscale IP address as
long as tailscaled.state is intact.
- Install Tailscale from the StartOS marketplace.
- A critical task will appear prompting you to Set Machine Name. This is the
hostname your node will advertise on your Tailscale network. The default is
startos. Accept the default or enter a custom name, then complete the task. - Start the service. The daemon is converged to your stored configuration via
tailscale up --hostname=<name> --login-server=<server>— the chosen machine name and any configured control server are applied automatically. - Open the Web Interface from the StartOS UI.
- Log in with your Tailscale account to join the node to your tailnet. If the node is waiting for interactive login, the authentication link is also shown in the Tailscale Daemon health message.
- Optionally configure subnet routes, exit node, or Tailscale SSH from the web interface.
- Use the Add Serve tile action on any installed service to expose it on your tailnet or the public internet.
No auth key or pre-configuration is required beyond the machine name step. All other setup happens interactively through the Tailscale web interface.
Alternatively, use the Login action (see Actions) to authenticate headlessly without a browser.
Using Headscale? Run the Control Server action and enter your Headscale server's URL (for example
https://headscale.example.com), then log in: run the Login action with a preauth key (headscale preauthkeys create --user <user>) to authenticate without a browser, or follow the authentication link shown in the Tailscale Daemon health message / web interface.
All Tailscale configuration is managed through the Tailscale web interface
(port 8080), which is the standard Tailscale device web UI (tailscale web).
| Feature | How to configure |
|---|---|
| Login / auth | Web UI → Sign in, or Login action |
| Machine name | Actions panel → Machine Name |
| Control server | Actions panel → Control Server |
| Subnet router | Web UI → Settings → Subnet router |
| Exit node | Web UI → This device → Exit node |
| Tailscale SSH | Web UI → Settings → Tailscale SSH server |
| Logout / re-auth | Web UI → Settings → Log out |
| Expose services | URL plugin → Add Serve (tile action, choose Serve or Funnel mode) |
The daemon is started with the following fixed arguments:
| Argument | Purpose |
|---|---|
--state=/var/lib/tailscale/tailscaled.state |
Persistent state file path |
--socket=/var/run/tailscale/tailscaled.sock |
Unix socket for CLI/web communication |
--tun=userspace-networking |
Userspace WireGuard (no kernel tun) |
| Interface | Type | Port | Description |
|---|---|---|---|
ui |
Web UI | 8080 | Tailscale device management web interface |
The Tailscale web interface is accessible over LAN and Tor via the standard StartOS interface mechanism on port 8080.
Tailscale itself uses no additional ports on the StartOS host. All WireGuard traffic is handled internally by tailscaled in userspace networking mode.
Note: Because userspace networking is used, outgoing connections from other containers to the wider internet via Tailscale require a SOCKS5 or HTTP proxy. Direct kernel-level routing (as available with
--tun) is not used.
Visible in the Actions panel. Works whether the service is running or stopped.
Sets the hostname this node advertises on your Tailscale network (and its MagicDNS name
if MagicDNS is enabled). The default is startos. All serve URLs
(e.g. mynode.tail1234.ts.net:10001) update automatically once Tailscale confirms
the new name.
Note: This action only has an effect when "Auto-generate from OS hostname" is enabled in the Tailscale admin console — this is the default for new machines. If you have manually renamed this machine in the admin console, the admin-console name takes precedence and this action will have no visible effect until you re-enable auto-generation there.
- On install: a critical task is automatically created, blocking startup until the user runs this action and confirms or changes the name.
- While stopped: stores the name; it is applied via
tailscale up --hostnamewhen the service next starts. - While running: stores the name and applies it immediately via
tailscale up --hostname— no restart needed. Serve URLs update as soon as Tailscale reflects the newSelf.DNSName. - Re-running this action (rename) stores the new name so it is re-confirmed on every start.
Visible in the Actions panel. Works whether the service is running or stopped.
Sets a custom control server URL, such as a self-hosted Headscale instance. Leave empty to use the official Tailscale control plane.
- While stopped: stores the URL; the daemon is converged via
tailscale up --login-server=<url>on next start. - While running: the node is re-authenticated against the new server live — no logout, no restart. With a configured auth key this is headless; otherwise the action returns an authentication link (also shown in the Tailscale Daemon health message) to complete login in a browser.
- Your machine name and serve configuration are preserved. Serves are re-applied automatically once the node is logged in on the new server.
- A stored auth key is cleared on change — it was issued for the previous control server and is meaningless against the new one.
With a custom control server, proxy serves are plain
http://(a Headscale server cannot provision the TLS certificates--httpsserves require) and Funnel is unavailable (Tailscale-cloud-only).
Visible in the Actions panel. Works whether the service is running or stopped.
Authenticates the node to your Tailscale network using an auth key. On first install, an important task is automatically created prompting you to run this action.
Input:
| Field | Required | Description |
|---|---|---|
| Auth Key | No | A tskey-auth-... key generated at https://login.tailscale.com/admin/settings/keys. With a custom control server, a preauth key from your Headscale server (headscale preauthkeys create). Leave blank to authenticate interactively instead. |
What it does internally:
- The auth key is always saved to
store.jsonwhen provided. - If the service is running and the daemon socket is reachable:
- Runs
tailscale up --hostname=<name> --login-server=<server> --auth-key=<key>. - Polls
tailscale status --jsonuntil the client is logged in. - Re-applies configured serves, refreshes
status.json, and clears the consumedauthKeyfrom the store.
- Runs
- If the service is stopped (socket unavailable): the key stays saved and
the startup ready-check runs the same
tailscale upautomatically on next start. - If no auth key is provided, the action still converges the daemon and returns the interactive authentication link when one is pending — it is also shown in the Tailscale Daemon health message and the web UI.
Key types and expiry:
Auth keys expire after 1–90 days (default 90). After expiry, the authenticated node remains connected until its node key expires (default 180 days). See the Tailscale auth key docs for details on reusable, ephemeral, pre-approved, and tagged keys.
These actions are exposed via the URL plugin (the "Add Serve" / "Remove Serve" table actions on service tiles in the StartOS UI). They are not visible in the Actions panel directly.
Add Serve assigns a tailnet port to a (packageId, interfaceId) pair and configures
either tailscale serve (tailnet-only) or tailscale funnel (public internet) to proxy
or forward traffic to that service.
Add Serve prompts you to choose a mode:
| Mode | Scope | Allowed ports | CLI command |
|---|---|---|---|
| Serve | Tailnet-private | Any unreserved port (≥10001) | tailscale serve |
| Funnel | Public internet | 443, 8443, 10000 only | tailscale funnel |
Serve (default) keeps traffic within your tailnet. Funnel makes the service
accessible to anyone on the public internet at https://<machine-name>.ts.net:<port>.
Warning: Funnel exposes your service to the open internet, including bot traffic and potential vulnerabilities. Only enable it for services that are designed to be public. Funnel must also be enabled for your tailnet in the Tailscale admin console.
- The assigned port starts at 10000 and increments by 1 above the current maximum.
- Custom port selection: an optional port number can be supplied when clicking "Add Serve" to override the auto-assigned value.
- Ports 80, 443, and 8080 are reserved by Tailscale and cannot be used for Serve entries.
- Port assignments are stable: the same port is reused whenever "Add Serve" is clicked again for an entry that was previously removed (the mapping is preserved as a legacy sentinel).
- Interface metadata (
hostId,scheme,internalPort) is read from the URL plugin prefill and cached instore.jsonso that subsequent startups do not require additional API calls. - The action is idempotent: if the entry already has a
hostIdrecorded, it returns immediately. If the entry exists but has nohostId(legacy record), it is upgraded in place.
How it works internally (Serve):
The scheme cached from addressInfo determines the tailscale serve target format
(--http instead of --https when a custom control server is configured):
scheme = 'http'or'ws'—tailscale serve --bg --https <port> http://<pkg>.startos:<internalPort>(Tailscale terminates TLS)scheme = 'https'or'wss'—tailscale serve --bg --https <port> https+insecure://<pkg>.startos:<internalPort>scheme = null/ TCP —tailscale serve --bg --tcp <port> tcp://<pkg>.startos:<internalPort>packageId = 'startos'—tailscale serve --bg --https <port> https+insecure://startos.startos:443
If the node is not logged in when Add Serve is clicked, the entry is saved and applied automatically once the client connects.
Funnel requires the official Tailscale control plane and is rejected when a custom control server (e.g. Headscale) is configured.
- Only ports 443, 8443, and 10000 are accepted (enforced by Tailscale upstream).
- Auto-assignment priority: 443 → 8443 → 10000 (first unused port in that order).
- Leaving the port field blank auto-assigns the first free funnel port.
- A custom port can be specified; it must be one of the three allowed funnel ports and must not be in use by another funnel entry.
- An existing funnel entry's port is preserved when the action is re-run for the same entry.
- The upstream target is always plain
http://— Tailscale terminates TLS externally. - The URL plugin tile shows these entries as Public (as opposed to Serve's private label).
How it works internally (Funnel):
tailscale funnel --bg --https=<port> http://<pkg>.startos:<internalPort>
Before re-applying, both tailscale serve reset and tailscale funnel reset are run
to atomically replace the entire configuration. The funnel reset is intentionally
non-fatal if it fails (e.g., Funnel is not enabled for the tailnet), while the serve
reset failure is fatal.
Legacy upgrade path: If you are upgrading from a prior version of this package,
existing entries in store.json will be detected as legacy (no hostId). The URL
plugin tile will continue to show "Add Serve". Click it once to supply the full
metadata — the existing tailnet port is preserved.
Included in backup:
tailscalevolume — daemon state, node identity, and keysstartosvolume — serve configuration (store.json)
Restore behavior:
- The node re-joins the tailnet with the same identity and Tailscale IP address.
- Configured serves are restored automatically on startup.
- No re-authentication is required after restore if the node has not been removed from the tailnet in the Tailscale admin console.
- If the node was removed from the tailnet, log in again via the web interface.
| Check | Display Name | Method |
|---|---|---|
tailscaled |
Tailscale Daemon | tailscale status --json exits 0 |
tailscale-web |
Web Interface | TCP port 8080 is listening |
Once per start, the tailscaled ready-check converges the daemon to the stored
configuration with tailscale up --hostname=<name> --login-server=<server>
(plus --auth-key when one is pending). While interactive login is pending, the
authentication link is shown in the health message. On every transition into the
logged-in state, configured serves are re-applied from store.json and
the node's Tailscale IP and MagicDNS name are written to startos/status.json,
which updates serve URLs in the Interfaces panel. The node then pings a few
online peers: a freshly (re-)registered node appears in its peers' netmaps
with no known endpoint or DERP home until it has sent something outbound, so
serves are unreachable until the first packet. Originating that traffic
immediately makes the node reachable within seconds of login instead of
whenever the first outbound connection happens to occur.
Logged in is not the same as
BackendState=Running. tailscaled reports Running whenever it wants to be up and holds a netmap, including for the whole of a re-authentication against a different control server — it keeps serving the outgoing tailnet's netmap while the control connection is torn down and the node re-registers. The package therefore treats the node as logged in only when BackendState is Running, no AuthURL is pending, and no login-state health warning is present. Serves written before that point would land in the profile tailscaled is about to discard.
None. Tailscale is a standalone service.
- Userspace networking only — kernel tun device is not used. Outbound routing for other containers requires SOCKS5 or HTTP proxy configuration.
- No CLI access — interact only through the web interface or StartOS
actions; the
tailscaleCLI is not exposed to the StartOS user directly. - No Taildrop — file transfer via Taildrop is not tested and not supported.
- HTTPS reverse proxy for HTTP services —
tailscale serve --bg --httpsis used for HTTP backends; Tailscale terminates TLS so clients access services overhttps://. Non-HTTP services use raw TCP forwarding. - Web UI requires Tailscale v1.56.0+ — the
ghcr.io/tailscale/tailscale:v1.104.4image satisfies this requirement. - Headscale caveats — with a custom control server set, serves are plain
http://(no TLS certificates) and Funnel is unavailable. Serve URLs fall back to the node's Tailscale IP if your Headscale server has no MagicDNS configured.
- WireGuard® encryption (automatic, always on)
- Tailnet mesh networking and DERP relay fallback
- MagicDNS
- Subnet router and exit node capabilities (configured via web UI)
- Tailscale SSH (configured via web UI)
- Node identity persistence across restarts
- Tailscale admin console compatibility
See CONTRIBUTING.md for build instructions and development workflow.
package_id: tailscale
image: ghcr.io/tailscale/tailscale:v1.104.4
architectures: [x86_64, aarch64]
volumes:
tailscale: /var/lib/tailscale # daemon state
startos: (js runtime) # store.json, status.json
ports:
8080: Tailscale web interface (HTTP)
dependencies: none
daemons:
- id: tailscaled
command: tailscaled --state=... --socket=... --tun=userspace-networking
health: tailscale status --json (socket responsive; pending AuthURL shown in the message)
on_start: |
once per start cycle (from store.json):
tailscale up --hostname=<machineName> --login-server=<controlServer|default> [--auth-key=<key>]
(retried once with --force-reauth when the daemon demands re-authentication)
on every transition into the logged-in state (Running, no AuthURL, no
login-state health warning):
applyServicesConfig (below), write ip + dnsName to startos/status.json,
clear consumed authKey from store.json
- id: tailscale-web
command: tailscale web --listen=0.0.0.0:8080
health: port 8080 listening
requires: [tailscaled]
applyServicesConfig: |
tailscale serve reset (fatal on failure)
tailscale funnel reset (non-fatal — logged as warning)
for each non-legacy entry (hostId != ''):
if mode=funnel:
tailscale funnel --bg --https <port> http://<pkg>.startos:<internalPort>
(skipped when controlServer is set — Funnel is Tailscale-cloud-only)
elif scheme=http/ws:
tailscale serve --bg --https <port> http://<pkg>.startos:<internalPort>
elif scheme=https/wss:
tailscale serve --bg --https <port> https+insecure://<pkg>.startos:<internalPort>
elif scheme=null/tcp:
tailscale serve --bg --tcp <port> tcp://<pkg>.startos:<internalPort>
packageId=startos: (same logic; host='startos')
(--http instead of --https when controlServer is set)
actions:
- id: add-serve # exposed via URL plugin table action
description: Assign a tailnet port and configure tailscale serve or funnel for a service interface
input:
urlPluginMetadata { packageId, interfaceId, hostId, internalPort }
mode (select): 'serve' (tailnet-only, default) or 'funnel' (public internet, ports 443/8443/10000 only)
optional_input: port (number) — custom port override; serve defaults to auto-assign from 10000, funnel defaults to 443→8443→10000
state: startos/store.json
schema: { packageId: { interfaceId: { port, hostId, scheme, internalPort, mode } } }
idempotent: skip if hostId already stored; upgrade legacy sentinel (hostId='') in place
- id: remove-serve # exposed via URL plugin table action
description: Remove a tailscale serve for a service interface
state: startos/store.json
- id: set-machine-name # visible in the Actions panel
name: Machine Name
description: Set the hostname advertised on the tailnet; critical first-run task blocks startup until completed
input: machineName (string, required, default 'startos') — 1–63 chars, lowercase letters/numbers/hyphens only
behavior: |
write machineName to startos/store.json
tailscale up --hostname=<name> --login-server=<controlServer|default> (applies live, any login state)
poll status until logged in, then re-apply serves and refresh status.json
if daemon stopped: startup ready-check applies the name on next start
- id: set-control-server # visible in the Actions panel
name: Control Server
description: Set a custom control server URL (e.g. Headscale); empty means the default Tailscale control plane
input: controlServer (string, optional) — http(s) URL
behavior: |
write controlServer to startos/store.json and clear any stored authKey
tailscale up --hostname=<name> --login-server=<url|default> (live; retried with
--force-reauth when the node was logged in to a different plane)
poll status until logged in: headless with a key, otherwise return the
pending AuthURL for browser login; serves re-apply automatically
- id: login # visible in the Actions panel
name: Login
description: Authenticate to Tailscale; optional auth key for headless login, or leave blank for interactive login
input: authKey (string, optional) — tskey-auth-... from admin console, or Headscale preauth key; leave blank for interactive login
behavior: |
always save authKey to startos/store.json when provided
tailscale up --hostname=<name> --login-server=<controlServer|default> [--auth-key=<key>]
poll status until logged in, then re-apply serves, refresh status.json,
and clear the consumed authKey from the store
if authKey blank: return the pending AuthURL for browser login
if daemon stopped: authKey persists in store.json; startup ready-check applies it