Back to Linux guides

Headscale: Self-Hosted VPN Control Server Setup

Stanley Ulili
Updated on February 11, 2026

In the modern world of distributed systems, cloud computing, and remote work, securely connecting devices and servers across different networks is a fundamental challenge. Tailscale has emerged as a popular solution, creating secure, zero-config virtual private networks (VPNs) using the robust WireGuard protocol. It simplifies networking by giving each device a stable IP address and handling complex firewall traversal. However, Tailscale's core functionality relies on a centralized coordination server, the "control plane," managed by Tailscale Inc. While convenient, this means relinquishing some control over your network's architecture.

What if you could have the power and simplicity of Tailscale but host the entire control plane yourself? This is where Headscale comes in. Headscale is a free, open-source, self-hosted implementation of the Tailscale control server. By running your own Headscale instance, you gain complete sovereignty over your encrypted network, making it ideal for self-hosters, hobbyists, and organizations with strict privacy or operational requirements. You are no longer dependent on an external service, which means your network can continue to function even if the internet is down (within your local network) or if the commercial provider changes its pricing or policies.

This article explores setting up and managing your own Headscale server, covering everything from the underlying concepts to installation using Docker. You'll discover how to configure the server, create users, register new devices (nodes), and establish secure connections between them. You'll also learn about securing your network with Access Control Lists (ACLs) and explore the optional web UI. By the end, you'll have the knowledge and confidence to build and control your own private, encrypted network from the ground up.

Understanding the core concepts: Tailscale and Headscale

Before diving into the command line, it's essential to understand the architecture you're building. Both Tailscale and Headscale are built on the same principles, but their management model is the key differentiator.

What is Tailscale?

Tailscale creates a secure, flat network, often called a "tailnet," over any physical network. Imagine all your devices (laptops, servers, phones, Raspberry Pis) being part of a single, private, and secure local area network (LAN), no matter where they are in the world.

This is achieved by separating the network into two main components:

The Data Plane is where the actual network traffic flows. Data is sent directly between your devices (peer-to-peer) whenever possible. This traffic is fully end-to-end encrypted using WireGuard, an extremely fast and modern VPN protocol. Tailscale ensures that only devices you have authorized can communicate.

The Control Plane is the coordination server. Its job is not to handle your data but to manage the network. It authenticates users and devices, manages public key distribution for encryption, and assigns IP addresses. When you use the commercial Tailscale service, their servers act as your control plane.

What is Headscale?

Headscale is a re-implementation of the Tailscale control plane. It allows you to run this coordination server on your own hardware. Your Tailscale clients (the software installed on your laptops, servers, etc.) will then communicate with your Headscale server instead of Tailscale's official servers.

This provides several significant advantages:

Complete Control & Sovereignty: You own the entire network infrastructure. You are not subject to a third party's terms of service, potential outages, or pricing changes.

Enhanced Privacy: Public keys and network metadata are stored on your server, not on a third-party's. While Tailscale has strong privacy policies, self-hosting provides the ultimate guarantee.

Offline & Air-Gapped Operation: You can create a tailnet that operates entirely on a local network without any internet connectivity, which is invaluable for secure labs, industrial settings, or home networks during an internet outage.

No Limits: You can add as many users and devices as your server can handle without worrying about subscription tiers or device limits imposed by a commercial plan.

Interestingly, Headscale was originally built by an employee of Tailscale. This speaks to the company's embrace of open-source principles and its understanding that a self-hosted option serves a valuable segment of the technical community that prioritizes ultimate control over convenience.

Headscale in action

Understanding what a functioning Headscale network looks like provides context for the setup process. In this example setup, there are three servers running in the cloud.

A list of three servers in the Hetzner cloud console.

The headscale server will run the Headscale control plane. The ubuntu-test and ubuntu-test-2 servers are client nodes that need to be added to the network. The goal is to allow these two test servers to communicate securely and directly with each other over the private tailnet created by Headscale.

Once configured, you can SSH into the main headscale server and list the nodes that have successfully registered with the control plane by executing a command inside the running Headscale Docker container:

 
docker exec headscale headscale nodes list

frame_00_47.jpg

The output of this command provides a table of all connected devices (nodes) in the tailnet. It shows crucial information such as the unique identifier for the node, the hostname of the device, the Headscale user that the device belongs to, the private IPv4 and IPv6 addresses assigned to the node by Headscale (these are the addresses used for communication), and the connection status.

From the ubuntu-test-2 machine, you can directly SSH into the ubuntu-test machine using the private IP address assigned by Headscale (e.g., 100.64.0.1):

 
ssh root@100.64.0.1

The connection succeeds, proving that the two nodes can communicate over the encrypted tailnet. You can do the same in reverse, connecting from ubuntu-test to ubuntu-test-2. However, if you try to connect to this private IP from a machine that is not part of the tailnet (like your local development machine), the connection will simply time out. This confirms that the network is private and inaccessible from the public internet.

Preparing your environment for Headscale

A robust setup requires some initial preparation. For this article, Docker and Docker Compose are used, as this is the most common and well-supported method for deploying Headscale.

Prerequisites

You need a Linux server with a public IP address. This could be a cloud VPS from providers like Hetzner, DigitalOcean, or Linode, or even a machine on your home network if it's publicly accessible.

Ensure that Docker and Docker Compose are installed on your server. This is the containerization platform used to run Headscale and its dependencies.

While you can use an IP address, using a domain or subdomain is highly recommended. This allows you to easily secure your server with HTTPS via TLS certificates. For example, headscale.yourdomain.com.

You must create a DNS A record for your chosen domain/subdomain that points to your server's public IP address.

Installing and configuring Headscale

With the environment prepared, you can proceed with the detailed installation. This involves creating the necessary configuration files and launching everything using Docker Compose.

Creating the directory structure and configuration files

First, SSH into your Headscale server and create a dedicated directory to hold all configurations and data:

 
mkdir ~/headscale
 
cd ~/headscale
 
mkdir config lib

Inside this headscale directory, you need to create three key files: docker-compose.yaml (defines the services: Headscale, Caddy, and a Web UI), config/config.yaml (the main configuration file for Headscale itself), and Caddyfile (the configuration for the Caddy reverse proxy).

Configuring the docker-compose.yaml file

This file is the blueprint for the entire application stack. It tells Docker Compose how to build and run the containers.

A close-up of the `headscale` service definition within the `docker-compose.yaml` file.

Here is a complete docker-compose.yaml file:

docker-compose.yaml
version: "3.7"

services:
  headscale:
    image: headscale/headscale:stable
    container_name: headscale
    restart: unless-stopped
    volumes:
      - ./config:/etc/headscale:ro
      - ./lib:/var/lib/headscale
    command: serve
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "headscale", "health"]
      interval: 10s
      timeout: 5s
      retries: 3

  headscale-ui:
    image: ghcr.io/gurucomputing/headscale-ui:latest
    container_name: headscale-ui
    restart: unless-stopped

  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - headscale
      - headscale-ui

volumes:
  caddy_data:
  caddy_config:

The headscale service uses the official headscale/headscale:stable image. The local ./config directory maps to /etc/headscale inside the container in read-only mode, and ./lib maps to /var/lib/headscale to persist the SQLite database and other stateful data. The serve command starts the Headscale server.

The headscale-ui service is an optional open-source web interface for managing Headscale. It doesn't need any ports exposed directly, as Caddy will handle that.

Caddy is a powerful and easy-to-use web server that acts as the reverse proxy. It will automatically handle HTTPS. The Caddyfile is mapped for configuration and named volumes are used for data and config persistence. The depends_on directive ensures Caddy starts after Headscale is ready.

Configuring Caddy with a Caddyfile

Create a file named Caddyfile in your ~/headscale directory. Replace headscale.yourdomain.com with your actual domain:

Caddyfile
https://headscale.yourdomain.com {
    reverse_proxy /web* http://headscale-ui:8080
    reverse_proxy * http://headscale:8080
}

The first line defines your site address. Caddy will automatically provision a free TLS certificate from Let's Encrypt for this address. The reverse_proxy /web* line tells Caddy that any request starting with /web should be forwarded to the headscale-ui container on its internal port 8080. The catch-all rule forwards all other traffic to the main headscale service, which is what the Tailscale clients need to communicate with.

Creating the Headscale config.yaml

This is the most critical file. Download the example configuration from the Headscale repository and save it as ~/headscale/config/config.yaml:

 
curl -o config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml

Now, open this file and edit two very important lines:

The server_url must be set to the public URL of your server, including the https://. This is the address that your Tailscale clients will use to find the control plane:

config.yaml
# Example configuration
server_url: https://headscale.pandor.cc

The `config.yaml` file, with the `server_url` line highlighted, showing a custom domain.

The policy.path defines where Headscale should look for your Access Control List (ACL) file:

config.yaml
# Find the policy section and set the path
policy:
  mode: file
  path: /etc/headscale/acl.hujson

Launching Headscale

You're now ready to bring your server to life. From the ~/headscale directory, run:

 
docker compose up -d

This command will pull the required Docker images and start all three services in the background. You can check the status with docker ps and view the logs with docker compose logs -f headscale.

Managing your tailnet: adding users and devices

Your Headscale server is running, but your network is empty. Adding users and connecting devices is the next step.

Creating a user

Headscale organizes devices under users (which function like namespaces). All commands are run by executing into the running headscale container:

 
docker exec headscale headscale users create tom

You can list all users to confirm the creation:

 
docker exec headscale headscale users list

Registering a new device (node)

To add a device to a user's network, you'll use a pre-authenticated key. This is a one-time use key that allows a device to register without needing an interactive browser login.

Generate the pre-auth key on the server. Create a key for the user tom. You can add an expiration flag (e.g., --expiration 1h) but by default, it lasts for one hour:

 
docker exec headscale headscale preauthkeys create --user tom

This will output a long key string starting with hskey-auth-. Copy this entire key.

Now, SSH into the client machine you want to add (e.g., ubuntu-test-3). Install the Tailscale client using the official script:

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

Use the tailscale up command with special flags to point it to your Headscale server and use your pre-auth key:

 
sudo tailscale up --login-server https://headscale.yourdomain.com --authkey <YOUR_PRE_AUTH_KEY>

The --login-server flag tells the client to use your Headscale server instead of the default Tailscale one. The --authkey flag provides the pre-authenticated key you just generated. After running this, the device will be registered and connected to your tailnet.

Verifying and adding more nodes

Go back to your Headscale server and list the nodes again:

The terminal output after adding a new device, showing three nodes in the list, with the new one assigned to the user 'tom'.

You will now see ubuntu-test-3 in the list, assigned to the user tom. Repeat the process for your other nodes (e.g., ubuntu-test-1). Remember to generate a new pre-auth key for each device you want to register.

Securing your network with ACLs

By default, Headscale doesn't allow any traffic between nodes. You must explicitly define who can talk to whom using an Access Control List (ACL) policy file. This is the acl.hujson file referenced earlier in config.yaml.

Create the file ~/headscale/config/acl.hujson.

A basic "allow all" policy

To get started, you can use a very permissive policy that allows all devices on the network to communicate with each other on any port.

The `acl.hujson` file containing the ACL and SSH rules.

acl.hujson
{
  "acls": [
    {
      "action": "accept",
      "src": ["*"],
      "dst": ["*:*"]
    }
  ],
  "ssh": [
    {
      "action": "accept",
      "src": ["autogroup:member"],
      "dst": ["autogroup:member"],
      "users": ["root"]
    }
  ]
}

The acls block rule {"action": "accept", "src": ["*"], "dst": ["*:*"]} means: allow any source to connect to any destination on any port. This is great for testing but should be restricted in a production environment.

The ssh block enables Tailscale SSH. It allows any authenticated member of your tailnet (autogroup:member) to SSH into any other member machine as the specified users (in this case, just root). This lets you use ssh root@<tailscale-ip> without managing SSH keys manually.

After saving this file, Headscale will automatically pick up the changes and apply the new policy, allowing your newly registered nodes to communicate.

Final thoughts

Headscale is an incredibly powerful tool that puts the control of a modern, secure mesh network squarely in your hands. While the initial setup is more involved than its commercial counterpart, the benefits of complete ownership, privacy, and flexibility are undeniable. The entire process covers understanding the architecture, deploying a multi-container application with Docker, configuring users and nodes, and defining security policies.

You now have a fully functional, self-hosted tailnet. You can connect your servers, development machines, and even Raspberry Pis into a single, seamless, encrypted network. The command-line interface provides granular control over every aspect of your network, and the open-source nature of the project means it's constantly evolving.

While some advanced Tailscale features like Funnel and Serve are still in development for Headscale, its core functionality is robust and production-ready. For anyone serious about self-hosting or building resilient, private network infrastructure, Headscale is an essential tool to master. You have now taken the first step towards true network sovereignty.