Deploy a Headscale control server
An open source Tailscale control server, to run your own private network without a Tailscale account.
In a Tailscale network, a tailnet, the control server registers the machines and shares their keys and addresses. The machines then connect to each other directly, over WireGuard. Headscale replaces the control server of Tailscale, and works with the same Tailscale clients.
This guide deploys Headscale with the Linux runtime and Mise, and stores its data in PostgreSQL. You then connect your devices to it, and your Clever Cloud applications with their built-in Tailscale support.
Prepare the project
You need Git, OpenSSL, Clever Tools, and a Clever Cloud account. Create a project and initialize its Git repository:
mkdir my-headscale
cd my-headscale
git initThe mise.toml file installs Headscale, and passes it the credentials of the PostgreSQL add-on, as Headscale reads a setting from a variable prefixed with HEADSCALE_. It also defines the run task, which the Linux runtime runs to start the application:
[tools]
"github:juanfont/headscale" = "0.29.7"
[env]
HEADSCALE_DATABASE_TYPE = "postgres"
HEADSCALE_DATABASE_POSTGRES_HOST = "{{ env.POSTGRESQL_ADDON_HOST }}"
HEADSCALE_DATABASE_POSTGRES_PORT = "{{ env.POSTGRESQL_ADDON_PORT }}"
HEADSCALE_DATABASE_POSTGRES_NAME = "{{ env.POSTGRESQL_ADDON_DB }}"
HEADSCALE_DATABASE_POSTGRES_USER = "{{ env.POSTGRESQL_ADDON_USER }}"
HEADSCALE_DATABASE_POSTGRES_PASS = "{{ env.POSTGRESQL_ADDON_PASSWORD }}"
HEADSCALE_DATABASE_POSTGRES_SSL = "require"
[tasks.run]
run = '''
#!/usr/bin/env bash
set -euo pipefail
: "${HEADSCALE_SERVER_URL:?Set HEADSCALE_SERVER_URL before deploying}"
: "${HS_NOISE_PRIVATE_KEY:?Set HS_NOISE_PRIVATE_KEY before deploying}"
mkdir -p data
printf '%s' "$HS_NOISE_PRIVATE_KEY" > data/noise_private.key
chmod 600 data/noise_private.key
headscale serve -c config.yaml
'''The task stops the deployment with an error if HEADSCALE_SERVER_URL or HS_NOISE_PRIVATE_KEY, which you set below, is missing. It then writes the private key of the server in data/noise_private.key, readable only by the application, and starts Headscale with config.yaml. This key comes from an environment variable, so that it stays the same across deployments: your machines keep trusting the server.
The config.yaml file holds the other settings of Headscale, which its documentation describes:
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
unix_socket: ./data/headscale.sock
unix_socket_permission: "0770"
noise:
private_key_path: ./data/noise_private.key
node:
ephemeral:
inactivity_timeout: 30m
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
disable_check_updates: true
dns:
magic_dns: true
base_domain: tailnet.internal
override_local_dns: false
nameservers:
global:
- 1.1.1.1Headscale listens on port 8080, behind the load balancers of Clever Cloud, which handle HTTPS. Its metrics and the socket of its command line stay local. It reads the private key that the run task writes, and gives machines addresses in the prefixes ranges.
An ephemeral machine stays registered for 30 minutes after it disconnects, instead of 2 by default, so that your applications stay in your network while Headscale redeploys, which takes a few minutes.
With dns, the machines name each other in the tailnet.internal domain, and keep their own DNS servers. When they can’t connect directly, they go through the public Designated Encrypted Relay for Packets (DERP) servers of Tailscale. An application can’t receive the UDP traffic, on port 3478, that the relay built into Headscale needs to work.
Create the application and its database
Create a Linux application, which runs on a single instance by default, then show its domain:
clever create --type linux my-headscale
clever config set zero-downtime false
clever domainHeadscale keeps the state of your network in memory, so only one server must run at a time. Disabling zero downtime deployment stops the previous instance before the new one starts: the server doesn’t answer during a deployment, then your machines reconnect to it.
Your machines keep the URL of the server. To use your own domain, add it now, and configure its DNS:
clever domain add headscale.example.comSet the public URL of your server, with your domain or the cleverapps.io one, then generate its private key:
HEADSCALE_URL="https://headscale.example.com"
clever env set HEADSCALE_SERVER_URL "$HEADSCALE_URL"
clever env set HS_NOISE_PRIVATE_KEY "privkey:$(openssl rand -hex 32)"Headscale recommends SQLite, and keeps PostgreSQL in maintenance mode. This guide uses PostgreSQL: the disk of an instance doesn’t persist across deployments, and SQLite advises against network file systems, such as an FS Bucket. Create the add-on, linked to the application. The dev plan suits a test: choose a larger plan for production.
clever addon create postgresql-addon my-headscale-pg --plan dev --link my-headscaleDeploy Headscale
Commit the project, then deploy it:
git add .
git commit -m "Deploy Headscale"
clever deployCheck that Headscale answers on its /health path, with the URL of your server:
curl "$HEADSCALE_URL/health"Create users and auth keys
Manage Headscale with its command line, on the instance, through SSH. Create a user, then list the users to get its ID:
clever ssh --command 'cd "$APP_HOME" && headscale -c config.yaml users create my-team'
clever ssh --command 'cd "$APP_HOME" && headscale -c config.yaml users list'Then create a reusable and ephemeral auth key for this user, here with the ID 1, as instances change with each deployment:
clever ssh --command 'cd "$APP_HOME" && headscale -c config.yaml preauthkeys create --user 1 \
--reusable --ephemeral --expiration 90d'The command returns the key, which starts with hskey-auth-.
Connect your machines
Without an access policy, every machine of your network reaches the others, whatever their user: define a policy to restrict access.
On a device, install the Tailscale client, then join your network as the Headscale documentation explains:
tailscale up --login-server https://headscale.example.com --auth-key hskey-auth-…For a device you keep, create its key without --ephemeral, so that it stays in your network while it’s offline.
To connect a Clever Cloud application, here my-app, set its Tailscale variables, not the ones of Headscale:
clever env set --app my-app TAILSCALE_LOGIN_SERVER "$HEADSCALE_URL"
clever env set --app my-app TAILSCALE_AUTH_KEY "hskey-auth-…"
clever restart --app my-appList the connected machines from the folder of your Headscale project:
clever ssh --command 'cd "$APP_HOME" && headscale -c config.yaml nodes list'