Hosting Headscale In a Container With Docker

What is Tailscale

Tailscale is a WireGuard-based modern VPN. Basically, it works as an overlay network in the computers between your networks — uses NAT traversal.

Its role is to be an exchange point of WireGuard public keys among Tailscale network nodes. It gives clients their IP addresses, sets the demarcation line between each user, allows users to share machines, and exposes your nodes' routes.

Based on private users or an organization, Tailscale assigns a user a private network (tailnet).

What is headscale?

Headscale is an open-source, self-hosted implementation of the Tailscale control server.

Headscale aims to implement a self-hosted, open-source alternative to the Tailscale control server. Headscale aims to provide self-hosters and hobbyists with an open-source server they can use for their projects and labs. It implements a narrower scope, a single Tailnet, suitable for personal use or a small open-source organization.

In this guide, we will be showing how to set up and run Headscale in a container.

Prerequisites

  • Docker installed
  • VPS
  • FQDN (although only an IP can also be used)
  • (Optional) Subdomain pointing to the IP of your VPS

Configure Headscale

First, make a directory on the host Docker node used to hold Headscale configuration and the SQLite database:

  • mkdir -p /root/.headscale/{config,lib,run}
  • cd /root/.headscale/config

Then, Download the example configuration for your chosen version and save it as: /root/.headscale/config/config.yaml

The lines that will definitely need modifying are:


server_url- Here, you will need to set the domain with a subdomain preceding it, like headscale.example.com or IP. Append the port on which you will be running the service.

listen_addr- For production, you will need to set this to your public IP, followed by the port.

metrics_listen_addr- This is best kept on your local/private network. The default is already configured as such.

tls_letsencrypt_hostname- This must be set to your server URL for the automatic TLS certificate. Omit if you are using only IP.

base_domain- Defines the base domain to create the hostnames for MagicDNS. So if your URL is headscale.example.com, you will need to enter example.com

Run Headscale

After the configuration is complete, start the Headscale server.

docker run \
  --name headscale \
  --net=host \
  --detach \
  --volume "/root/.headscale/config:/etc/headscale" \
  --volume "/root/.headscale/lib:/var/lib/headscale" \
  --volume "/root/.headscale/run:/var/run/headscale" \
  --no-healthcheck \
  docker.io/headscale/headscale:<VERSION> \
  serve

Replace <VERSION> with the version of headscale you would like to run.

ℹ️
As you may have noticed, we have stopped the health checks for this container. As of writing of this guide i.e. 15.11.2025, the headscale container doesn't have any kind of shell. The --health-cmd flag used to check health of containers requires the existence of /bin/sh , thus the health check automatically fails and the container is marked as unhealthy. Possible solutions for this would be to run a healthcheck from the host OS with the command docker exec -it headscale headscale health or making a custom container with sh included.


And finally, verify that Headscale is running:

Follow the container logs:

  • docker logs --follow headscale

If no errors occurred, you might get an output similar to this:

Verify running containers:

  • docker ps

Verify Headscale is available:

  • curl http://127.0.0.1:9090/metrics - If Headscale is running correctly, by running this command, you will get a lot of metrics regarding the container.


Post-Install Setup

We will use Tailnet as a client for our Headscale server.
We will create two users to test out connectivity between them.

docker exec -it headscale headscale users create user1

docker exec -it headscale headscale users create user2

We can take a look at the created users with the command:

docker exec -it headscale headscale users list


Register a machine (normal login)

To register a machine when running Headscale in a container, take the Headscale command and pass it to the container:

docker exec -it headscale \
  headscale nodes register --user <USER_ID> --key <YOUR_MACHINE_KEY>

Register a machine using a pre-authenticated key

Generate a key using the command line:

docker exec -it headscale \
  headscale preauthkeys create --user <USER_ID> --reusable --expiration 24h
ℹ️
Replace <USER_ID> with the actual ID of the user, which can be seen via the output of the users list command as shown above


This will return a pre-authenticated key that can be used to connect a node to Headscale

Downloading the client and logging in

Go to tailscale.com and download the client for your OS, then open a terminal (or PowerShell if you are running Windows) and run the following commands:

Authenticating using a pre-authenticated key:

  • tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>

Authenticating using a normal login

  • tailscale up --login-server YOUR_HEADSCALE_URL

We can run the command

docker exec -it headscale headscale nodes list

To check the nodes that are currently connected and their IPs. I have connected two nodes to Headscale.

We can now run a simple ping test to determine if the setup is working.

First VM pinging second VM
Second VM pinging first VM


Debugging headscale running in Docker

The headscale/headscale Docker container is based on a "distroless" image that does not contain a shell or any other debug tools. If you need to debug your application running in a Docker container, you can use the -debug variant, for example headscale/headscale:x.x.x-debug.

Running the debug Docker container

To run the debug Docker container, use the exact same commands as above, but replace headscale/headscale:x.x.x with headscale/headscale:x.x.x-debug (x.x.x is the version of Headscale). The two containers are compatible with each other, so you can alternate between them.

Executing commands in the debug container

The default command in the debug container is to run headscale, which is located at /ko-app/headscale inside the container.

Additionally, the debug container includes a minimalist Busybox shell.

To launch a shell in the container, use:

docker run -it headscale/headscale:x.x.x-debug sh

You can also execute commands directly, such as ls /ko-app In this example:

docker run headscale/headscale:x.x.x-debug ls /ko-app

Using docker exec -it allows you to run commands in an existing container.

Conclusion

In this guide, we successfully installed and configured a self-hosted Headcale Docker container. We also learned how to create users and connect to our Headscale node using Tailscale. Along the way, we touched on some intricacies in using a containerized Headscale. Now you are ready to establish WireGuard-based connections between your services!