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.
Mentions of this being a free service in the video below are now no longer correct:
- To launch a server, a credit card is now required (a temporary charge between $1-$10 will be taken to verify it)
- Fly.io no longer offer free dedicated IPv4 addresses, these are now charged at $2/month for each IPv4 address.
- Fly.io gives new accounts $5 of free trial credit and Hobby plans cost $5 per month.
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.
- Setting up your own self hosted remote access
- Video
- Installation
- Fly.io client
- Fly.io deployment
- Launching the Fly.io app
- Setting up the Fly.io docker-wireguard-tunnel
- Install Docker community edition via the convenience script
- Configuring Headscale
- Joining Tailscale clients to the network using your custom control server URL
- Docker compose setup to run Tailscale on Linux
- Additional resources
Video
Watch on YouTubeInstallation
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
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.