Skip to main content

Tailscale & Headscale

Setting up your own self hosted remote access

Headscale is an open source implementation of the Tailscale coordination server.

This guide will step through setting up your own self hosted private and secure remote access using Tailscale clients along with a self hosted Headscale Docker container.

warning

Mentions of this being a free service in the video below are now no longer correct:

So in theory as long as you don't exceed the $5 free trial credit, this setup should still be free. Make sure that you check their current free allowances, as they may have changed since this guide was last published.

Video

Watch on YouTube

Installation

Fly.io client

sudo apt install curl
curl -L https://fly.io/install.sh | sh
echo 'export FLYCTL_INSTALL="$HOME/.fly"' >> ~/.bashrc
echo 'export PATH="$FLYCTL_INSTALL/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Fly.io deployment

mkdir fly-tunnel
cd fly-tunnel
nano fly.toml

Note: you can update choose-a-subdomain-1234 and my-user to any value you'd like to use.

# fly.toml app configuration file
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = "choose-a-subdomain-1234"

[build]
image = "ghcr.io/digitallyrefined/docker-wireguard-tunnel:v3"

[env]
DOMAIN = "choose-a-subdomain-1234.fly.dev"
PEERS = "1"
SERVICES = "peer1:headscale:80:80,peer1:headscale:443:443"

[[mounts]]
source = "wireguard_data"
destination = "/etc/wireguard"

[[services]]
protocol = "udp"
internal_port = 51820

[[services.ports]]
port = 51820

[[services]]
protocol = "tcp"
internal_port = 80

[[services.ports]]
port = 80

[[services]]
protocol = "tcp"
internal_port = 443

[[services.ports]]
port = 443

Launching the Fly.io app

Sign up for a Fly.io account

warning

Make sure that you check Fly.io's current free allowances, as they may have changed since this guide was last published.

This setup uses 1 Fly.io app. As long as you don't have more than 3 Fly.io apps running you wont be exceeding Fly.io's free allowance in terms of server usage, however dedicated IPv4 addresses are now a paid for feature, so you will need to add a credit card to launch this app. You can use a temporary credit card from privacy.com in the US or Revolut in some parts of Europe.

fly auth login
fly launch

Use the following options:

? Would you like to copy its configuration to the new app? Yes
? Choose an app name (leaving blank will default to 'choose-a-subdomain-1234') choose-a-subdomain-1234
? Choose a region for deployment: Denver, Colorado (US) (den) # Or a location closest to you
? Would you like to set up a Postgresql database now? No
? Would you like to set up an Upstash Redis database now? No
? Would you like to deploy now? Yes
? Would you like to allocate a dedicated ipv4 address now? Yes

Setting up the Fly.io docker-wireguard-tunnel

Once the Fly.io tunnel has started, a peer1.conf file will be automatically generated in the /etc/wireguard directory, it can be viewed and then removed via:

fly ssh console
cat /etc/wireguard/peer1.conf
# Copy the contents of peer1.conf
rm /etc/wireguard/peer1.conf

Now we'll create a new folder called headscale/fly-wireguard and copy peer1.conf to a new file called wg0.conf.

mkdir -p ~/headscale/fly-wireguard
nano ~/headscale/fly-wireguard/wg0.conf

Install Docker community edition via the convenience script

curl -fsSL https://get.docker.com | sudo bash
sudo usermod -aG docker $USER

Configuring Headscale

Create a headscale folder, import the default configuration and tweak

cd ~/headscale
mkdir config
wget -O ./config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml
nano config/config.yaml

Settings to update in the config/config.yaml file:

server_url: https://choose-a-subdomain-1234.fly.dev:443
listen_addr: 0.0.0.0:443
acme_email: "you@example.com"
tls_letsencrypt_hostname: "choose-a-subdomain-1234.fly.dev"
nano docker-compose.yml
version: "2"
services:
headscale:
container_name: headscale
image: headscale/headscale
command: "headscale serve"
restart: unless-stopped
volumes:
- ./config:/etc/headscale/
- ./data:/var/lib/headscale/

fly-wireguard-peer:
container_name: headscale-fly-wireguard-peer
image: ghcr.io/digitallyrefined/docker-wireguard-tunnel:v3
restart: unless-stopped
environment:
# Note that DOMAIN & PEERS are not required for the peer
# Services to expose format (comma-separated)
# SERVICES=peer-id:peer-container-name:peer-container-port:expose-port-as
- SERVICES=peer1:headscale:80:80,peer1:headscale:443:443
cap_add:
- NET_ADMIN
links:
- headscale:headscale
volumes:
- ./fly-wireguard:/etc/wireguard
docker compose up -d
docker compose logs -f

After a few minutes you should now be able to access your new Headscale server by going to your subdomain https://choose-a-subdomain-1234.fly.dev/swagger

Create a new user for your Headscale server

docker exec headscale headscale users create my-user

Joining Tailscale clients to the network using your custom control server URL

Registering a new device and allowing it to join your network

docker exec headscale headscale nodes register --user my-user --key nodekey:...

Docker compose setup to run Tailscale on Linux

Create a new preauth key

docker exec headscale headscale --user my-user preauthkeys create --expiration 1h

docker-compose.yml

mkdir ~/tailscale
cd ~/tailscale
nano docker-compose.yml

Update the --login-server URL and --advertise-routes to match your network, then add the preauth key to the TS_AUTHKEY environment variable (this key only used once on first run to join the network).

services:
tailscale:
container_name: tailscale
image: tailscale/tailscale:stable
hostname: headtailscale
volumes:
- ./data:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
network_mode: "host"
cap_add:
- NET_ADMIN
- NET_RAW
environment:
- TS_STATE_DIR=/var/lib/tailscale
- TS_EXTRA_ARGS=--login-server=https://choose-a-subdomain-1234.fly.dev --advertise-exit-node --advertise-routes=192.168.x.0/24 --accept-dns=true
- TS_NO_LOGS_NO_SUPPORT=true
# - TS_AUTHKEY=...
restart: unless-stopped
docker compose up -d
docker compose logs -f

docker exec headscale headscale nodes list
docker exec headscale headscale routes list

docker exec headscale headscale routes enable -r 1
docker exec headscale headscale routes enable -r 2 # 3 etc

docker exec headscale headscale routes list

Now each of client should be able connect with each other and access local network resources if you have enabled --advertise-routes=192.168.x.0/24 and also be able to use your home internet connect while you're away via Tailscale -advertise-exit-node argument.

Additional resources