Previously, I accessed my NAS at home through port mapping, but there were some issues:
- Need to configure DDNS, there may be a delay when switching IP
- Accessing web services requires adding ports
- Only expose certain services, cannot open public network access to core services (such as router configuration)
Recently, I have researched and tried various networking solutions (ZeroTier/WireGuard/Tailscale), and finally chose to build my own Headscale solution.
Preparation#
- A VPS, with Docker and Docker Compose installed, and the following ports open:
8080
Headscale HTTP9090
Headscale Metrics8443
DERP server HTTPS3478/udp
DERP server STUN8008
Headscale-Admin
- Client (Linux/macOS/Windows/iOS/Android)
Deploy DERP Server#
Create a directory to store the configuration files for the DERP server
mkdir -p /app/derper/certs
cd /app/derper
Create a self-signed certificate
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout /app/derper/certs/<hostname>.key -out /app/derper/certs/<hostname>.crt -subj "/CN=<hostname>" -addext "subjectAltName=DNS:<hostname>"
hostname
can be any value, it just needs to be consistent
Create a docker-compose.yaml
file with the following content:
services:
derper:
image: ghcr.nju.edu.cn/yangchuansheng/derper:latest
container_name: derper
restart: always
volumes:
- /app/derper/certs:/app/certs
environment:
- DERP_CERT_MODE=manual
- DERP_ADDR=:443
- DERP_DOMAIN=<hostname>
ports:
- 8443:443
- 3478:3478/udp
Here, we are using the GHCR image provided by Nanjing University. If not needed, you can replace
ghcr.nju.edu.cn
withghcr.io
Finally, start the DERP server
docker compose pull
docker compose up -d
Create a derp.json
configuration file and upload it to a location that can be accessed directly:
{
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "cn-hangzhou",
"RegionName": "Hangzhou",
"Nodes": [
{
"Name": "901h",
"RegionID": 901,
"DERPPort": 8443,
"HostName": "<hostname>",
"IPv4": "<IP>",
"InsecureForTests": true
}
]
}
}
}
Here, we assume that we have uploaded it to
https://example.com/derp.json
Deploy Headscale#
Create a directory for the Headscale configuration files
mkdir -p /app/headscale
cd /app/headscale
mkdir config
touch config/db.sqlite
wget -O config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml
Modify the configuration in the config/config.yaml
file:
- Modify
server_url
tohttp://<IP>:8080
- Modify
listen_addr
to0.0.0.0:8080
- Modify
metrics_listen_addr
to0.0.0.0:9090
(optional) - Modify
grpc_listen_addr
to0.0.0.0:50443
- Modify
noise.private_key_path
to/etc/headscale/noise_private.key
- Modify
prefixes.v4
andprefixes.v6
as needed - Modify
derp.urls[0]
to the URL of the uploadedderp.json
file, which ishttps://example.com/derp.json
in this case - Modify
database.sqlite.path
to/etc/headscale/db.sqlite
- Modify
dns_config.nameservers
to223.5.5.5
and223.6.6.6
(modify according to your situation) - Modify
dns_config.base_domain
to your own domain name, which will be displayed in the client
Create a docker-compose.yaml
file:
services:
headscale:
image: headscale/headscale:0.23.0-alpha8
container_name: headscale
restart: always
command: serve
volumes:
- ./config:/etc/headscale
ports:
- 8080:8080
- 9090:9090
Finally, start Headscale
docker compose pull
docker compose up -d
Deploy Headscale-Admin#
Create a directory for the Headscale-Admin configuration files
mkdir -p /app/headscale-admin
cd /app/headscale-admin
Due to the HTTP security restrictions of browsers, direct access through a domain name will result in cross-origin issues. Here, we use Caddy as a reverse proxy to bypass this issue.
nano Caddyfile
The content is as follows:
:{$PORT:80} {
reverse_proxy /api/* <IP>:8080
root * /app
encode gzip zstd
try_files {path}.html {path}
file_server
}
Create a docker-compose.yaml
file:
services:
headscale-admin:
image: goodieshq/headscale-admin:latest
container_name: headscale-admin
restart: unless-stopped
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
environment:
- PORT=80
- ENDPOINT=/headscale
ports:
- 8008:80
Finally, start it
docker compose pull
docker compose up -d
The API Key for Headscale can be created with the following command:
docker exec -it headscale headscale apikey create
The Web UI can be accessed at http://<IP>:8008/headscale
, and the Legacy API
option should be unchecked in the configuration.
Create Users#
Execute the following command on the server
docker exec -it headscale headscale users create <your username>
Client Configuration#
Linux#
First, install the Tailscale client
curl -fsSL https://tailscale.com/install.sh | sh
Then, request to join the Headscale network
tailscale up --login-server=http://<IP>:8080 --accept-routes
If you need to access other networks through this node, add the --advertise-routes
parameter:
tailscale up --login-server=http://<IP>:8080 --advertise-routes=192.168.1.0/24 --accept-routes
Open the link in the returned result, there is an authentication command inside, execute it on the server
docker exec headscale headscale nodes register --user <your username> --key nodekey:******
Apple Devices#
Access http://<IP>:8080/apple
directly and follow the steps.
Android#
Open F-Droid and download the APK installation package:
f-droid.org/en/packages/com.tailscale.ipn
After installation, open the app, tap the three dots in the upper right corner multiple times, and the Change Server
option will appear. Enter http://<IP>:8080
and follow the prompts to complete the authentication.
Windows#
I don't have a Windows device at home, so I can't test and verify it. Please refer to the tutorials in the reference materials.