Tuxvador's Blog
Automated Daily CrowdSec Reports via Email Managing DNS Programmatically with the IONOS API Getting Started with Proxmox VE for Your Home Lab My Proxmox VE Homelab: Infrastructure Overview Monitoring Your Homelab with Grafana, Loki, and CrowdSec

Using Headscale for Self-Hosted VPN Access


Tailscale makes VPNs simple — it creates a WireGuard-based mesh network where every device gets a unique IP and can reach every other device directly. The catch is that Tailscale relies on their control server for coordination.

Headscale is the open-source answer: it implements the Tailscale control protocol so you can run your own coordination server. This means no dependency on an external service and full control over your mesh network.

Architecture

┌─────────────────┐      WireGuard       ┌──────────────────┐
│  Laptop (home)  │ ◄──────────────────► │  Headscale Server │
│  100.x.x.1      │                      │  vpn-srv:8080     │
└─────────────────┘                      └────────┬─────────┘
         ▲                                         │
         │            WireGuard mesh               │
         ▼                                         │
┌─────────────────┐                                │
│  Phone (4G)     │ ◄──────────────────────────────┘
│  100.x.x.2      │
└─────────────────┘

Each node connects to the Headscale server, gets a unique IP in the 100.x.x.x/10 range, and can reach other nodes directly via WireGuard (peer-to-peer where possible, relayed via DERP when NAT traversal fails).

Setting Up Headscale

On a dedicated LXC container:

# Download latest release
wget https://github.com/juanfont/headscale/releases/latest/download/headscale_0.23.0_linux_amd64.deb
dpkg -i headscale_0.23.0_linux_amd64.deb

# Configure
nano /etc/headscale/config.yaml

Key config settings:

server_url: https://vpn.tuxvador.fr:8080
listen_addr: 0.0.0.0:8080
grpc_listen_addr: 0.0.0.0:50443
private_key_path: /var/lib/headscale/private.key
derp:
  server:
    enabled: true
    stun_listen_addr: 0.0.0.0:3478
dns_config:
  base_domain: headscale.home.lab

Enable and start:

systemctl enable --now headscale

Adding Nodes

On the Headscale server:

headscale users create homelab
headscale preauthkeys create -user homelab -expiration 24h

On each client (laptop, phone, server):

Install the Tailscale client and point it at your Headscale server:

curl -fsSL https://tailscale.com/install.sh | sh
tailscale up --login-server=https://vpn.tuxvador.fr:8080 --authkey <your-key>

The device registers with your server and gets an IP. Repeat for every node you want in the mesh.

Verifying the Mesh

tailscale status

You should see all connected nodes and their IPs. Test connectivity:

ping 100.x.x.2

Routing to Internal Services

The real value is accessing internal services that aren’t exposed to the internet. If your blog runs on dev-srv:5000, add a Tailscale subnet route:

On your dev-srv container:

tailscale up --login-server=https://vpn.tuxvador.fr:8080 --advertise-routes=10.99.99.0/24

Then on the Headscale server, enable the route:

headscale routes enable -r <route-id>

Now from your laptop on Tailscale, you can reach http://10.99.99.20:5000 directly — no reverse proxy or port forwarding needed.

Why Self-Host?

  • No dependency: Tailscale’s SaaS could go down or change terms. Your Headscale server is yours.
  • Full control: Logs, keys, and authentication stay within your infrastructure.
  • Custom DNS: Set up your own domain resolution within the mesh.
  • Air-gapped networks: Works entirely offline if needed.

Resource Usage

Headscale is extremely lightweight. On my Debian 13 container:

  • ~50 MB RAM with 5 active nodes
  • Minimal CPU — only busy during node registration and key exchanges
  • WireGuard handles the actual traffic, so throughput depends on your link, not Headscale

For a homelab, this is the perfect VPN solution — no subscription, no external dependency, and WireGuard’s performance is excellent.