Skip to main content

Tailscale and Headscale

Clouduler does not talk to Tailscale or Headscale itself. A tailnet is just a network your nodes can use: the Tailscale client creates a tailscale0 interface with a private 100.x.x.x address on each machine, and you tell Clouduler to use those addresses as its internal network. This is how you run one cluster across several sites. This guide covers enrolling a machine into a tailnet, running your own control server (Headscale) as a stack on the cluster, and where to go for the full multi-site walkthrough.

What you'll end up with​

  • Machines enrolled in a tailnet, either on Tailscale's hosted service or on your own Headscale.
  • Optionally Headscale running as a Clouduler stack, reachable at https://headscale.example.com.

Prerequisites​

  • Linux machines with systemd and root access.
  • For your own Headscale: a Clouduler cluster, a public network with a real domain, and ideally Authentik for sign-in (Headscale example).

Install the Tailscale client​

On Debian, Ubuntu and Raspberry Pi OS:

curl -fsSL https://tailscale.com/install.sh | sh

On Arch Linux, install the tailscale package and start the service:

sudo pacman -S tailscale
sudo systemctl enable --now tailscaled

You should now see tailscaled running: systemctl status tailscaled.

Join Tailscale's hosted tailnet​

sudo tailscale up

This prints a login URL. Open it in a browser and approve the machine. Check with tailscale status and tailscale ip -4.

Join your own Headscale​

Headscale is a self-hosted Tailscale control server. The client is the same; you point it at your server with --login-server.

Option A: approve in the browser​

sudo tailscale up --login-server https://headscale.example.com --accept-dns=false

It prints a URL containing an auth ID. On the Headscale side approve the node:

headscale auth register --user <USER> --auth-id <AUTH_ID>

Option B: a pre-auth key (no browser)​

On the Headscale side, create a user once and a key:

headscale users create cluster
headscale users list
headscale preauthkeys create --user <USER_ID>

The default key is single-use and valid for one hour. Run headscale preauthkeys create --help for the expiry, reusable and --tags flags of your version, since they have changed between releases. On the machine:

sudo tailscale up --login-server https://headscale.example.com --auth-key=<KEY> --accept-dns=false

You should now see the machine in tailscale status, and tailscale ip -4 prints its address, for example 100.64.0.2.

Flags are not remembered

Tailscale does not persist tailscale up flags between runs. Repeat --login-server and --accept-dns=false on every later tailscale up, or the next one silently goes back to the default control server. If the machine was logged into another control server before, add --force-reauth.

Why --accept-dns=false on cluster nodes

With MagicDNS and override_local_dns on, Headscale can rewrite the node's resolver, which Clouduler's own DNS and the node rely on. Turning DNS acceptance off on cluster nodes is the safe default.

Run Headscale on the cluster​

The Headscale example has the full stack (Headscale and its admin UI Headplane, with sign-in through Authentik). The Clouduler-specific points are:

  • Publish it on a public network so every client can reach https://headscale.example.com over the internet without the tailnet. That route is what clients log in against.

  • Pin it to one stable node, ideally one with a public address, and back up its volume (db.sqlite, noise_private.key).

  • Create the first user and key inside the container:

    clouduler exec headscale.headscale.0 -- headscale users create admin
    clouduler exec headscale.headscale.0 -- headscale preauthkeys create --user <USER_ID>
  • Never put the Docker socket of nodes you do not control behind Headplane: a writable Docker socket is root on the node.

Use the tailnet as Clouduler's network​

Read each node's tailnet address with tailscale ip -4 and use it as the internal address in cluster.yaml; do not guess it, because Headscale hands addresses out sequentially. All three cluster ports (7373, 7374, 7375 by default) then run over tailscale0.

Order matters

The internal network has to exist before the cluster forms, but a tailnet hosted by a stack on the cluster does not exist until the cluster does. A running node's internal address can be changed with a config edit (see Changing a node's address), but only in the order the multi-site quick start walks through: the new network must be up on every node first.

See Multi-site with Headscale for the whole path, including what happens after a total outage. Change the tailscale MTU before the first stack deploy: Docker overlay networks pin their MTU when created, and tailscale0 defaults to 1280.

Test it​

  • tailscale status lists the other machines.
  • ping 100.64.0.2 from another enrolled machine answers.
  • On the Headscale side, headscale nodes list shows the node.
  • Behind a NAT, direct connections need UDP 41641 outbound; otherwise Tailscale falls back to relays over outbound 443.

Troubleshooting​

You seeCauseFix
tailscale up goes to tailscale.com instead of your server--login-server was left out on a later run.Repeat all flags every time.
Login URL opens but the node never appearsYou forgot to approve it.Run headscale auth register --user <USER> --auth-id <AUTH_ID>.
Pre-auth key rejectedIt is single-use and expires after an hour by default.Create a new one, or check headscale preauthkeys create --help.
Cannot reach Headscale after a power cutHeadscale is a stack and needs the cluster to be scheduled.See the outage section of the multi-site quick start and Emergency mode.
Overlay networks break after changing MTUDocker pins the MTU at creation.Set the MTU before the first deploy.