Headscale: The Self-Hosted Alternative to Tailscale
Video
Transcript
Hello everyone, welcome back to Easy Self Host.
Today, we're going to set up Headscale, which is the open-source, self-hosted alternative to Tailscale's control server.
In our last video, we used Tailscale VPN to access applications on our home server.
If you haven't seen it yet, be sure to check out that video first, as we'll be reusing many of the concepts and tools from it.
A key component of Tailscale VPN is its control server, which orchestrates the VPN nodes and provides services like authentication, overlay networking, and DNS.
Headscale is the self-hosted alternative to Tailscale's control server.
By hosting it on a server with a public IP address, we can continue using Tailscale's client software—like mobile apps and Docker containers—with our Headscale server.
Alright, let's get started with running Headscale.
The first thing we need is a server with a public IP address.
I'm going to use DigitalOcean since I'm already familiar with it.
You can choose any VPS provider that fits your region and pricing preferences.
Headscale doesn't require a lot of resources, so I'll pick the lowest-tier virtual server.
Let's go ahead and launch the server.
The official Headscale documentation recommends running Headscale without any HTTP proxy or container.
However, I'm going to use both a proxy and a Docker container to run Headscale.
Using a proxy allows you to run multiple web services on a single host, maximizing the value of your VPS.
Running Headscale in Docker makes it easier to maintain.
After the server is up, the first thing I'll do is to install Docker.
I'll install Docker using the package manager.
First, let's run `apt update` to update the package registry.
Then, run `apt install docker.io` to install the Docker engine.
We also need to run `apt install docker-compose-v2` to install Docker Compose.
Now, let's move on to configuring and running Headscale on the server:
I'll be writing all the Headscale configuration on GitHub and downloading it to my server later.
You can also use any editor to write your configuration and upload it to the server.
Here's the Docker Compose file we'll need to run Headscale.
It creates two volumes to persist data for both Caddy and Headscale.
Our first service is Caddy, which is our proxy server using the official Caddy image.
We need to map ports 80 and 443 to handle HTTP and HTTPS traffic.
For volumes, the `caddy` volume will persist all the data under the `/data` directory.
We also need to map the Caddyfile to the configuration path inside the container.
The next service is the Headscale server, which uses the official image.
We need to set the command to `serve`, so it runs the server when the container starts.
For volumes, the `headscale` volume will persist the data.
And we also map the configuration from the current directory to the configuration path inside the container.
Now, let's start writing the configuration file for Headscale.
We can find the latest configuration template on the Headscale website.
Let's copy that template and paste it into our editor.
Then, we can begin customizing it for our server.
The first field we need to change is the `server_url`.
This is the URL we'll use to connect to our Headscale server.
I'm planning to use the domain `headscale.cloud.easyselfhost.com`.
Remember, we need to set up a DNS record for this domain so it resolves to our server's IP address.
The `listen_addr` should be set to `0.0.0.0:8080`, which means Headscale will listen on all IP addresses on port 8080 within the container.
The `metrics_listen_addr` is for accessing server metrics.
I'll keep it set to `localhost` since we don't need external access to metrics.
The same goes for `grpc_listen_addr`. If you don't need to use the remote Headscale command-line tool, you can keep it on `localhost`.
Moving forward, we can leave the `noise` key path at its default value.
The next section is for the VPN IP prefixes.
The private IP addresses within the Headscale VPN will be generated based on these prefixes.
We can keep both the IPv4 and IPv6 prefixes at their default values.
The next section is for DERP, and it's disabled by default.
For our use case, we don't need this service, so we can leave it disabled.
We can keep the default values for most of the remaining configurations until we reach the DNS section.
Similar to Tailscale, Headscale also supports Magic DNS, which assigns a domain name to each VPN node.
Tailscale uses `ts.net` as the base domain.
With Headscale, we can customize this domain.
Although we don't need to own this domain, I recommend choosing one that doesn't resolve to any IP address.
You can check this using the command-line tool `nslookup`.
For example, short domains like `z.net` or `a.net` don't have DNS records.
But `example.com` does resolve to an IP address.
For my setup, I'll use the domain `ezsh.net`.
The `nameservers` section defines the DNS servers in the Headscale network.
`Global` is for the default DNS servers, and the values listed are Cloudflare's DNS IPs.
We don't need to change any of these values for now.
Headscale supports adding custom DNS records via the `extra_records` field.
We can use this to resolve our home server domains within the Headscale network later, so we won't need to run another DNS server.
Let's remove any deprecated fields from the configuration.
OpenID Connect support in Headscale is still experimental, so I'm not going to use it at this time.
The Caddyfile for the Headscale server is quite simple.
We just need to proxy the domain name to the Headscale service on port 8080.
Caddy will automatically handle the TLS certificate if we're using a public IP address.
That’s all for the configuration.
Before we get started to run Headscale server, make sure the domain name resolves to the IP of our VPS.
Now, I'm going to clone my Git repository and navigate to the directory that contains the Headscale Docker Compose file.
From here, let's bring up the Docker Compose services.
After that, we can run `docker compose logs -f` to view the logs of both services and check if they're running successfully.
Headscale doesn't have a built-in admin graphic UI.
To manage it, we'll need to use its command-line tool.
Since we're running it inside a container, we'll need to run `docker exec headscale headscale` to execute commands.
Now, let's connect our phone to the Headscale network.
The first thing we need to do is create a user for the phone.
The command is `headscale users create` followed by the username.
On your phone, if you've previously used the Tailscale app to connect to Tailscale, you'll need to log out of your account.
Additionally, go into the Tailscale settings and enable "Reset Keychain".
Now, open the Tailscale app and tap "Log in", then tap the three dots in the top right corner.
Select "Use a custom coordination server".
Here, enter your Headscale server URL.
After this, you'll be prompted with a web page from your server containing a command to register your phone.
Copy this command and paste it into your server terminal.
Remember to add the `docker exec headscale` prefix and replace `USERNAME` with `phone`.
Run the command, and you'll notice that Tailscale logs in and the web view closes on your phone.
In the accounts page, you can see that you're now using the Headscale network `ezsh.net`.
On the server, run the command `headscale nodes list`. You'll see that the only node right now is your phone.
You might notice that its name is "localhost".
But we can change that using the command `headscale nodes rename` followed by the new name you want, using the identifier `1`.
Now, this node is renamed to "phone".
This change is also reflected in the phone app.
Now let's connect our home server to Headscale using the Tailscale Docker container.
First, we'll create another user for our home server.