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:
8080Headscale HTTP9090Headscale Metrics8443DERP server HTTPS3478/udpDERP server STUN8008Headscale-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>"
hostnamecan 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.cnwithghcr.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_urltohttp://<IP>:8080 - Modify
listen_addrto0.0.0.0:8080 - Modify
metrics_listen_addrto0.0.0.0:9090(optional) - Modify
grpc_listen_addrto0.0.0.0:50443 - Modify
noise.private_key_pathto/etc/headscale/noise_private.key - Modify
prefixes.v4andprefixes.v6as needed - Modify
derp.urls[0]to the URL of the uploadedderp.jsonfile, which ishttps://example.com/derp.jsonin this case - Modify
database.sqlite.pathto/etc/headscale/db.sqlite - Modify
dns_config.nameserversto223.5.5.5and223.6.6.6(modify according to your situation) - Modify
dns_config.base_domainto 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.