Introduction#
Managing remote access to your homelab can be challenging. Traditional VPN solutions often require complex configurations, port forwarding, and expose your network to potential security risks. Tailscale offers an elegant solution with its mesh networking approach, but relies on their coordination servers. Enter Headscale - a self-hosted, open-source implementation of the Tailscale coordination server that gives you complete control over your network.
In this guide, we’ll set up Headscale on a VPS and connect both your homelab and laptop for secure, seamless access from anywhere.
Why Headscale + Tailscale?#
The Problem with Traditional Approaches#
- Port forwarding exposes services directly to the internet
- Traditional VPNs require complex routing, a public IP address and DNS might be challenging
- SSH tunnels are manual and don’t scale well
- Cloud VPN services create vendor lock-in and privacy concerns
The Headscale Solution#
- Zero-trust networking: Each device authenticates independently
- NAT traversal: Works behind firewalls and routers without configuration
- Automatic routing: Devices find the most efficient path to communicate
- Self-hosted control: You own the coordination server and all metadata
- Easy device management: Simple commands to add/remove devices
Headscale is a self-hosted, open-source implementation of the Tailscale coordination server that gives you complete control over your network. Tailscale works by creating a mesh network of devices that can communicate with each other. That means that you only need to have one server where every client connects to. Then, if possible, the clients try to build a direct connection to each other.
Client traffic is always end-to-end encrypted, but you can even restrict through which servers the clients are relaying the packets by using your own DERP servers. The DERP protocol is used to establish a secure connection between Tailscale nodes. It is explained in more detail here.
Architecture Overview#
architecture-beta
group tailscale(internet)[Internet]
group homelab(server)[homelab]
group wifi(internet)[public wifi]
service laptop(disk)[Laptop] in wifi
service server1(database)[Homelab Server] in homelab
service server2(database)[Homelab Server] in homelab
service headscale(server)[headscale] in tailscale
service nginx(server)[nginx] in homelab
service internet(cloud)[internet] in tailscale
service labnet(cloud)[Labnet] in homelab
nginx:L -- R:labnet
server1:B -- T:labnet
server2:T -- B:labnet
nginx:R -- L:internet
laptop:T -- B:internet
headscale:B <-- T:internet
Only the headscale server is exposed to the internet. And after the initial connection to the headscale server, the clients try to connect directly using the DERP protocol. The lab servers are only reachable through the nginx, which is also a tailscale client.
Setup#
install#
Follow the installation instructions on the Headscale website and install it on your VPS.
For me the current version was 0.26.1 and I use an amd64 bit architecture. You can find your architecture by running uname -m, lscpu or arch.
HEADSCALE_VERSION="0.26.1"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install ./headscale.deb
configure#
headscale#
I have setup headscale.kohnkenet.de to point to the IP of my VPS publicly. Then configure the config.yaml file according to your need. In my case I want to server it on HTTPS:
sudo nano /etc/headscale/config.yaml
I have changed the following settings:
server_url: https://headscale.kohnkenet.de:443
listen_addr: 0.0.0.0:443
acme_email: "cert@****.de"
tls_letsencrypt_hostname: "headscale.kohnkenet.de"
ufw#
Your UFW should already be setup on the VPS. I will allow the following ports to
sudo ufw status
sudo ufw allow 3478/udp
sudo ufw allow https
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
3478/udp ALLOW Anywhere
443 ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere (v6)
3478/udp (v6) ALLOW Anywhere (v6)
443 (v6) ALLOW Anywhere (v6)
verify#
use the following commands to verify that everything is working as expected:
sudo netstat -lnpt
sudo ss -tulnp | grep headscale
sudo headscale user list
sudo headscale node list
It should look something like this:
ssh@vps1:~$ sudo netstat -lnpt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.54:53 0.0.0.0:* LISTEN 3795/systemd-resolv
tcp 0 0 127.0.0.1:9090 0.0.0.0:* LISTEN 18742/headscale
tcp 0 0 127.0.0.1:50443 0.0.0.0:* LISTEN 18742/headscale
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1/systemd
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 3795/systemd-resolv
tcp6 0 0 :::22 :::* LISTEN 1/systemd
tcp6 0 0 :::80 :::* LISTEN 18742/headscale
tcp6 0 0 :::443 :::* LISTEN 18742/headscale
ssh@vps1:~$ sudo ss -tulnp | grep headscale
udp UNCONN 0 0 *:3478 *:* users:(("headscale",pid=18742,fd=10))
tcp LISTEN 0 128 127.0.0.1:9090 0.0.0.0:* users:(("headscale",pid=18742,fd=17))
tcp LISTEN 0 128 127.0.0.1:50443 0.0.0.0:* users:(("headscale",pid=18742,fd=15))
tcp LISTEN 0 128 *:80 *:* users:(("headscale",pid=18742,fd=14))
tcp LISTEN 0 128 *:443 *:* users:(("headscale",pid=18742,fd=16))
connect#
To connect to your homelab, you can use the Tailscale client on your laptop or mobile device. Follow the instructions on the Tailscale website to install the client and then run the following command to connect:
sudo tailscale up --login-server https://headscale.kohnkenet.de
This will give you a command to copy and enter in the hadscale server, something like this:
headscale nodes register --user nginx --key YYa-VKuiFPpZqdsafasdgw
you can verify the connection on the server like this:
ssh@vps1:~$ sudo headscale user list
ID | Name | Username | Email | Created
1 | | bjarne | | 2025-09-28 18:24:23
3 | | nginx | | 2025-09-28 18:24:24
ssh@vps1:~$ sudo headscale node list
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Connected | Expired
1 | nginx | nginx | [jsUnm] | [gfdAS] | nginx | 100.64.0.2, fd7a:115c:a1e0::2 | false | 2025-09-28 19:11:07 | N/A | online | no
create extra records#
In my case I want to route HTTP/S traffic through my lab nginx server. As you can see in the output above, the server got the VPN IP 100.64.0.2. In the homelab I would create a DNS record for the hostname nginx.kohnkenet.de and point it to the IP 100.64.0.2. In Tailscale we create an extra_record in the headscale config.yaml.
Create one entry for every website where the nginx is the reverse proxy. In my example:
- nginx.kohnkenet.de
- git.kohnkenet.de
# Extra DNS records
# so far only A and AAAA records are supported (on the tailscale side)
# See: docs/ref/dns.md
extra_records:
- name: "nginx.kohnkenet.de"
type: "A"
value: "100.64.0.2"
- name: "nginx.kohnkenet.de"
type: "AAAA"
value: "fd7a:115c:a1e0::2"
- name: "git.kohnkenet.de"
type: "A"
value: "100.64.0.2"
- name: "git.kohnkenet.de"
type: "AAAA"
value: "fd7a:115c:a1e0::2"
