How to install tailscale on XCP-NG host

By installing tailscale on XCP-NG hosts, you can provide easier access to your virtualization host using VPN.

Run the following commands via SSH as root on the XCP-NG host:

sudo yum-config-manager --add-repo https://pkgs.tailscale.com/stable/centos/7/tailscale.repo
sudo yum -y install tailscale

and enable & start the tailscale daemon tailscaled:

systemctl enable --now tailscaled


Posted by Uli Köhler in Headscale, Networking, Virtualization, VPN

How to install tailscale on Fedora CoreOS

In order to install tailscale, on Fedora CoreOS (this post has been tested on Fedora CoreOS 35), you can use this sequence of commands:

sudo curl -o /etc/yum.repos.d/tailscale.repo https://pkgs.tailscale.com/stable/fedora/tailscale.repo
sudo rpm-ostree install tailscale

Now reboot using

sudo systemctl reboot

Once rebooted, you can enable the service using

sudo systemctl enable --now tailscaled

and then configure tailscale as usual:

sudo tailscale up --login-server .... --authkey ...

Also see our post on How to connect tailscale to headscale server on Linux

Posted by Uli Köhler in CoreOS, Headscale, VPN

How to connect Synology NAS to Headscale

First, install the Tailscale App using the Synology Package manager. Don’t try to initialize using the UI since this will only work with the commercial tailscale service, not with headscale.

Then login to the NAS using SSH (I’m using the admin account) and run sudo su to run

You should see the following shell prompt:


Now you can initialize tailscale using the tailscale command similar to our previous post How to connect tailscale to headscale server on Linux. In my case, I needed to use the --reset flag in order for the command to work.

tailscale up --reset --login-server https://headscale.mydomain.com --authkey ... --accept-routes

This will login to your server just like the normal (non-synology) tailscale client does.

Posted by Uli Köhler in Headscale, VPN

How to install tailscale on Ubuntu

In order to instal tailscale, on any Ubuntu version, you can use the official tailscale install command:

sudo apt -y install curl apt-transport-https
curl -fsSL https://tailscale.com/install.sh | sh
Posted by Uli Köhler in Headscale, Raspberry Pi, VPN

Headscale nginx reverse proxy config

Reverse proxying headscale using nginx is extremly simple. You don’t need any special config.

Here is my config with Let’s Encrypt enabled (part of the config is auto-generated using certbot --nginx).

Ensure to set the port (27896) to match the one mapped to the Headscale port.

server {
    server_name  headscale.mydomain.com;

    location / {
        proxy_pass http://localhost:27896/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_redirect default;

    listen [::]:443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/mydomain-wildcard/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/mydomain-wildcard/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot

server {
    if ($host = headscale.mydomain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    server_name  headscale.mydomain.com;

    listen [::]:80; # managed by Certbot
    return 404; # managed by Certbot

The proxy_… upgrade stuff is for proxying websockets. I have no idea if it’s required or not because I have not tried, but it doesn’t really hurt to keep in in there.

Posted by Uli Köhler in Headscale, Networking, nginx

Headscale docker-compose config with PostgreSQL

This config is intended for larger installations than our sqlite based standard config. It tends to be slightly easier to back up correctly and will be faster for larger workloads. However, it will consume more RAM especially for low-workload installations and you have two docker containers to worry about during maintenance (though they are managed using a single docker-compose instance). I do not recommend using a shared postgres server although this is certainly possible.

First, create a random password using

echo POSTGRES_PASSWORD=$(pwgen 30 1) > .env

The docker-compose.yml looks like this:

version: '3.5'
    image: headscale/headscale:latest
      - ./config:/etc/headscale/
      - ./data:/var/lib/headscale
      - 27896:8080
    command: headscale serve
    restart: unless-stopped
      - postgres
    image: postgres
    restart: unless-stopped
      - ./pg_data:/var/lib/postgresql/data
      - POSTGRES_DB=headscale
      - POSTGRES_USER=headscale


Now we create the default headscale config:

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

In config/config.yaml we need to make these changes:

Set server URL:

server_url: https://headscale.mydomain.com

Comment out the sqlite database (add # to the front of every line):

# SQLite config
# db_type: sqlite3
# db_path: /var/lib/headscale/db.sqlite

And uncomment and configure postgres:

# Postgres config
db_type: postgres
db_host: postgres
db_port: 5432
db_name: headscale
db_user: headscale
db_pass: ohngooFaciice2hooGoo1Ahvif3ahl

Make sure all of these are uncommented and you copy the password from .env . It is extremely important that you use a unique password here to prevent attacks from unprivileged host processes to the docker containers.

My recommendation is to reverse proxy headscale using traefik or nginx instead of using the builtin Let’s Encrypt / ACME support. This will allow not only sharing the port & IP address with other services, standard services like Traefik and/or nginx are much more well tested regarding exposure to the internet and hence provide a potential security benefit. Additionally, they make it easier to manage certificates in a service-independent manner and provide an additional layer for debugging etc.

You might also configure custom IP address ranges:

  - fd5d:7b60:4742::/48

but this is optional.

For more info regarding autostart etc, see How to setup headscale server in 5 minutes using docker-compose

Posted by Uli Köhler in Headscale, Networking, VPN

How to create namespace on headscale server

Currently you need to create a namespace using the command line.

If running without a container

headscale namespaces create -n mynamespace

If running using docker-compose:

docker-compose exec headscale headscale namespaces create mynamespace

If successful, this will show

Namespace created
Posted by Uli Köhler in Headscale, Networking, VPN

How to list headscale namespaces

To list namespaces (which are comparable to accounts) in headscale, run

headscale namespaces list

If you are using headscale using docker-compose, use e.g.

docker-compose exec headscale headscale namespaces list

Example output

ID | Name | Created            
1  | uli  | 2022-01-16 19:54:55
Posted by Uli Köhler in Headscale, Networking, VPN

How to connect tailscale to headscale server on Linux

Also see our guide on How to setup headscale server in 5 minutes using docker-compose

Assuming you are running your headscale server at https://headscale.mydomain.com and you have already created a namespace named mynamespace, use one of the following methods:

Pre-Authkeys method (recommended)

First, create a pre-authkey token which is valid for 24h on the server:

headscale preauthkeys create -e 24h -n mynamespace

or (docker-compose version)

docker-compose exec headscale headscale preauthkeys create -e 24h -n mynamespace

This will generate a pre-auth key such as 3215a1ce7967c11e8ea844b3e199d3c46f9f5e7b660b48fb which you can send to the user.

Now login on the client using

tailscale up --login-server https://headscale.mydomain.com --authkey 3215a1ce7967c11e8ea844b3e199d3c46f9f5e7b660b48fb

Direct login method

tailscale up --login-server https://headscale.mydomain.com

On the client, this will show you an URL to access using your browser on the headscale server. This will in turn give you a command that you need to run on the host running the headscale container. If running headscale using docker-compose, prepend docker-compose exec headscale to the command and replace NAMESPACE by the name of your namespace.

The only reason why this method is not recommended by me is because it requires back-and-forth interaction between the user and the administrator which I don’t consider practical.

Posted by Uli Köhler in Headscale, Linux, Networking, VPN

How to setup headscale server in 5 minutes using docker-compose

This headscale setup is using sqlite – with a much lighter memory & CPU footprint than PostgreSQL for simple usecases, I recommend this for almost any installation: Headscale doesn’t have to manage that many requests and using sqlite3 is fine for all but the most demanding setups.

First, create the directory where headscale and all the data will reside in (we use /opt/headscale in this example).

sudo mkdir -p /opt/headscale

Now run the following script in /opt/headscale to initialize the files and directories headscale requires:

mkdir -p ./config
touch ./config/db.sqlite
curl https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -o ./config/config.yaml

docker-compose config

Now it’s time to create /opt/headscale/docker-compose.yml:

version: '3.5'
    image: headscale/headscale:latest
      - ./config:/etc/headscale/
      - ./data:/var/lib/headscale
      - 27896:8080
    command: headscale serve
    restart: unless-stopped

This will configure headscale to run its HTTP server on port 27896. You can reverse proxy this port to the domain of your choice.


Now we should edit the server name in config/config.yaml:

server_url: https://headscale.mydomain.com

Note that you need to restart tailscale after each

Next, see How to create namespace on headscale server for details on how you can create a namespace. Once you have created a namespace (comparable to an account on the commercial tailscale service), you can continue connecting clients (the client software is called tailscale), see e.g. How to connect tailscale to headscale server on Linux


Using the method described in our previous post Create a systemd service for your docker-compose project in 10 seconds we will now setup autostart on boot for headscale using systemd. This command will also start it immediately:

curl -fsSL https://techoverflow.net/scripts/create-docker-compose-service.sh | sudo bash /dev/stdin

How to view the logs

Use this command to view & follow the logs:

docker-compose logs -f

Example output

headscale_1  | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
headscale_1  | 
headscale_1  | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
headscale_1  |  - using env:    export GIN_MODE=release
headscale_1  |  - using code:   gin.SetMode(gin.ReleaseMode)
headscale_1  | 
headscale_1  | [GIN-debug] GET    /metrics                  --> github.com/zsais/go-gin-prometheus.prometheusHandler.func1 (4 handlers)
headscale_1  | [GIN-debug] GET    /health                   --> github.com/juanfont/headscale.(*Headscale).Serve.func2 (4 handlers)
headscale_1  | [GIN-debug] GET    /key                      --> github.com/juanfont/headscale.(*Headscale).KeyHandler-fm (4 handlers)
headscale_1  | [GIN-debug] GET    /register                 --> github.com/juanfont/headscale.(*Headscale).RegisterWebAPI-fm (4 handlers)
headscale_1  | [GIN-debug] POST   /machine/:id/map          --> github.com/juanfont/headscale.(*Headscale).PollNetMapHandler-fm (4 handlers)
headscale_1  | [GIN-debug] POST   /machine/:id              --> github.com/juanfont/headscale.(*Headscale).RegistrationHandler-fm (4 handlers)
headscale_1  | [GIN-debug] GET    /oidc/register/:mkey      --> github.com/juanfont/headscale.(*Headscale).RegisterOIDC-fm (4 handlers)
headscale_1  | [GIN-debug] GET    /oidc/callback            --> github.com/juanfont/headscale.(*Headscale).OIDCCallback-fm (4 handlers)
headscale_1  | [GIN-debug] GET    /apple                    --> github.com/juanfont/headscale.(*Headscale).AppleMobileConfig-fm (4 handlers)
headscale_1  | [GIN-debug] GET    /apple/:platform          --> github.com/juanfont/headscale.(*Headscale).ApplePlatformConfig-fm (4 handlers)
headscale_1  | [GIN-debug] GET    /swagger                  --> github.com/juanfont/headscale.SwaggerUI (4 handlers)
headscale_1  | [GIN-debug] GET    /swagger/v1/openapiv2.json --> github.com/juanfont/headscale.SwaggerAPIv1 (4 handlers)
headscale_1  | [GIN-debug] GET    /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] POST   /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] PUT    /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] PATCH  /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] HEAD   /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] OPTIONS /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] DELETE /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] CONNECT /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | [GIN-debug] TRACE  /api/v1/*any              --> github.com/gin-gonic/gin.WrapF.func1 (5 handlers)
headscale_1  | 2022-01-16T19:04:04Z WRN Listening without TLS but ServerURL does not start with http://
headscale_1  | 2022-01-16T19:04:04Z INF listening and serving (multiplexed HTTP and gRPC) on:
headscale_1  | 2022-01-16T19:04:04Z INF Setting up a DERPMap update worker frequency=86400000


Posted by Uli Köhler in Headscale, Networking, VPN, Wireguard