Skip to content

About

The easiest, most secure way to use WireGuard and 2FA on StartOS.

Resources

Contributing

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Tailscale Logo

Tailscale on StartOS

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.


Table of Contents


Image and Container Runtime

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):

  1. tailscaled — the Tailscale daemon, running in userspace networking mode
  2. tailscale-web — runs tailscale web --listen=0.0.0.0:8080, exposing the management UI

tailscale-web starts only after tailscaled is ready.


Volume and Data Layout

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 membership
  • startos/store.json — top-level object containing machineName (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; null means the default Tailscale control plane), and serves (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.


Installation and First-Run Flow

  1. Install Tailscale from the StartOS marketplace.
  2. 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.
  3. 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.
  4. Open the Web Interface from the StartOS UI.
  5. 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.
  6. Optionally configure subnet routes, exit node, or Tailscale SSH from the web interface.
  7. 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.


Configuration Management

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)

Daemon environment

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)

Network Access and Interfaces

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.


Actions (StartOS UI)

Machine Name

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 --hostname when 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 new Self.DNSName.
  • Re-running this action (rename) stores the new name so it is re-confirmed on every start.

Control Server

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 --https serves require) and Funnel is unavailable (Tailscale-cloud-only).

Login

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.json when provided.
  • If the service is running and the daemon socket is reachable:
    1. Runs tailscale up --hostname=<name> --login-server=<server> --auth-key=<key>.
    2. Polls tailscale status --json until the client is logged in.
    3. Re-applies configured serves, refreshes status.json, and clears the consumed authKey from the store.
  • If the service is stopped (socket unavailable): the key stays saved and the startup ready-check runs the same tailscale up automatically 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.

Add Serve / Remove Serve

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.

Mode selector

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.

Serve mode (tailnet-only)

  • 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 in store.json so that subsequent startups do not require additional API calls.
  • The action is idempotent: if the entry already has a hostId recorded, it returns immediately. If the entry exists but has no hostId (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 mode (public internet)

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.


Backups and Restore

Included in backup:

  • tailscale volume — daemon state, node identity, and keys
  • startos volume — 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.

Health Checks

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.


Dependencies

None. Tailscale is a standalone service.


Limitations and Differences

  1. Userspace networking only — kernel tun device is not used. Outbound routing for other containers requires SOCKS5 or HTTP proxy configuration.
  2. No CLI access — interact only through the web interface or StartOS actions; the tailscale CLI is not exposed to the StartOS user directly.
  3. No Taildrop — file transfer via Taildrop is not tested and not supported.
  4. HTTPS reverse proxy for HTTP services — tailscale serve --bg --https is used for HTTP backends; Tailscale terminates TLS so clients access services over https://. Non-HTTP services use raw TCP forwarding.
  5. Web UI requires Tailscale v1.56.0+ — the ghcr.io/tailscale/tailscale:v1.104.4 image satisfies this requirement.
  6. 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.

What Is Unchanged from Upstream

  • 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

Contributing

See CONTRIBUTING.md for build instructions and development workflow.


Quick Reference for AI Consumers

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

About

The easiest, most secure way to use WireGuard and 2FA on StartOS.

Resources

Contributing

Stars

1 star

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages