Dein eigenes VPN: Headscale auf einem eigenen Server einrichten
Worum es geht
Tailscale ist ein hervorragendes Mesh-VPN, aber es setzt voraus, dass du Tailscale Inc. als Anbieter für die Verwaltung deiner Geräte (den sogenannten "Control-Server") vertraust. Headscale ist eine quelloffene Neuimplementierung genau dieses Control-Servers, kompatibel mit den offiziellen Tailscale-Apps auf all deinen Geräten, nur eben komplett selbst gehostet.
Diese Anleitung zeigt dir, wie du:
- einen eigenen Headscale-Server auf einem günstigen VPS aufsetzt
- einen eigenen DERP-Relay betreibst (das Fallback-Netzwerk für Verbindungen, die kein direktes Peer-to-Peer schaffen, z.B. hinter restriktivem NAT), damit du wirklich unabhängig von Tailscales Infrastruktur bist
- Familie oder Freunden Zugriff auf deine selbst gehosteten Dienste gibst
- deine eigenen Geräte und die anderer Personen sauber verwaltest
Wichtig: Diese Anleitung geht von einer Installation auf einem VPS mit Debian 13 aus. Mit anderen Distributionen oder Versionen können einzelne Befehle abweichen.
Voraussetzungen: Du solltest ein Terminal bedienen können, per SSH auf deinem Server eingeloggt sein und dort einen eigenen Benutzer mit sudo-Rechten haben. Falls du noch keinen passenden Server hast, zeigt dir der optionale Bonus-Teil Server-Einrichtung, wie du bei Hetzner einen Debian-13-VPS aufsetzt. Ein Sysadmin musst du für all das nicht sein.
Was du außerdem brauchst: eine eigene Domain, bei der du DNS-Einträge setzen kannst.
Teil 1: Headscale und einen eigenen DERP-Relay installieren
Alles läuft bei uns über Docker, das hält den Server aufgeräumt und macht Updates einfach. Als Reverse Proxy nutzen wir Caddy, der sich automatisch um Let's-Encrypt-Zertifikate kümmert.
Docker installieren
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER
Kurz aus- und wieder einloggen, damit die Docker-Gruppenmitgliedschaft greift.
Caddy installieren
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Verzeichnisstruktur anlegen
mkdir -p ~/headscale/headscale/config
mkdir -p ~/headscale/headscale/data
mkdir -p ~/headscale/derper/certs
cd ~/headscale
Domains, die wir brauchen
Für den Rest der Anleitung gehen wir von folgendem Domain-Schema aus (alles unter deiner eigenen Domain, ersetze example.com entsprechend):
| Domain | Zweck |
|---|---|
vpn.example.com |
Headscale-Control-Server (öffentlich) |
derp.example.com |
Eigener DERP-Relay (öffentlich, nur der Port zählt) |
internal.example.com |
MagicDNS-Basisdomain (nur intern, nie öffentlich nutzen, siehe Warnkasten unten) |
⚠️ Wichtig: Die MagicDNS-Basisdomain (
internal.example.com) darf niemals für andere, öffentlich erreichbare Dienste verwendet werden. Headscale "übernimmt" für Geräte in deinem Tailnet jede Anfrage unter dieser Domain und beantwortet sie selbst (mit "nicht gefunden", falls kein passendes Gerät existiert) — unabhängig davon, was im echten, öffentlichen DNS steht. Öffentliche Dienste (siehe Teil 4) gehören auf eine andere Subdomain oder direkt auf deine Hauptdomain.
Docker-Compose-Stack
# ~/headscale/docker-compose.yml
networks:
vpn:
driver: bridge
services:
headscale:
image: headscale/headscale:0.29.2
container_name: headscale
restart: unless-stopped
command: serve
volumes:
- ./headscale/config:/etc/headscale
- ./headscale/data:/var/lib/headscale
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:9090:9090"
networks:
- vpn
mem_limit: 256m
derper:
build: ./derper
container_name: derper
restart: unless-stopped
command: >
--hostname=derp.example.com
--certmode=manual
--certdir=/app/certs
-a=:8443
--stun-port=3478
--verify-clients=false
volumes:
- ./derper/certs:/app/certs
ports:
- "8443:8443"
- "3478:3478/udp"
networks:
- vpn
mem_limit: 128m
Wichtig: Immer eine konkrete Versionsnummer angeben (hier 0.29.2), niemals :latest. Headscale ändert zwischen Minor-Versionen gelegentlich die Config-Struktur, mit einem festen Tag behältst du die Kontrolle darüber, wann du updatest.
DERP-Relay bauen
Der offizielle Tailscale-DERP-Server (derper) wird aus dem Quellcode gebaut:
# ~/headscale/derper/Dockerfile
FROM golang:1.22-alpine AS build
RUN apk add --no-cache git ca-certificates
ENV GOTOOLCHAIN=auto
RUN go install tailscale.com/cmd/derper@latest
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /go/bin/derper /usr/local/bin/derper
ENTRYPOINT ["/usr/local/bin/derper"]
GOTOOLCHAIN=auto sorgt dafür, dass Go sich bei Bedarf selbst eine passende Compiler-Version nachlädt, falls das Basis-Image zu alt ist.
Headscale-Konfiguration
# ~/headscale/headscale/config/config.yaml
server_url: https://vpn.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 0.0.0.0:50443
grpc_allow_insecure: false
trusted_proxies: []
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
derp:
server:
enabled: false
urls: []
paths:
- /etc/headscale/derp.yaml
auto_update_enabled: false
update_frequency: 3h
disable_check_updates: false
node:
expiry: 0
ephemeral:
inactivity_timeout: 30m
routes:
ha:
probe_interval: 10s
probe_timeout: 5s
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
tls_letsencrypt_hostname: ""
tls_cert_path: ""
tls_key_path: ""
log:
level: info
format: text
policy:
mode: file
path: ""
dns:
magic_dns: true
base_domain: internal.example.com
override_local_dns: true
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
search_domains: []
extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
Eigene DERP-Map, damit Headscale unseren Relay statt Tailscales öffentliche Server nutzt:
# ~/headscale/headscale/config/derp.yaml
regions:
900:
regionid: 900
regioncode: custom
regionname: "Eigener DERP-Relay"
nodes:
- name: "900a"
regionid: 900
hostname: derp.example.com
stunport: 3478
stunonly: false
derpport: 8443
Caddy konfigurieren
# /etc/caddy/Caddyfile
vpn.example.com {
reverse_proxy 127.0.0.1:8080
}
# Dient nur der Zertifikatsbeschaffung, der eigentliche DERP-Traffic
# läuft direkt auf Port 8443 an Caddy vorbei.
derp.example.com {
respond 404
}
sudo systemctl reload caddy
Alles starten
cd ~/headscale
docker compose up -d
DERP-Zertifikat einrichten
Das ist der etwas kniffligste Teil: Let's-Encrypt-Zertifikate werden immer über Port 80/443 validiert, unabhängig davon, auf welchem Port dein Dienst tatsächlich läuft. Da Caddy diese Ports bereits belegt, holt Caddy das Zertifikat für derp.example.com (der respond 404-Block von eben dient genau dafür), und ein kleines Skript kopiert es regelmäßig zu derper:
#!/bin/bash
# ~/headscale/derper/sync-cert.sh
set -euo pipefail
SRC="/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/derp.example.com"
DEST="/home/deinname/headscale/derper/certs"
cp "$SRC/derp.example.com.crt" "$DEST/derp.example.com.crt"
cp "$SRC/derp.example.com.key" "$DEST/derp.example.com.key"
chmod 644 "$DEST/derp.example.com.crt"
chmod 600 "$DEST/derp.example.com.key"
cd /home/deinname/headscale
docker compose up -d --force-recreate derper
chmod +x ~/headscale/derper/sync-cert.sh
sudo ~/headscale/derper/sync-cert.sh
Damit das Zertifikat auch nach der automatischen Erneuerung durch Caddy (alle paar Wochen) übernommen wird, richten wir einen wöchentlichen Cronjob ein:
sudo crontab -e
Zeile hinzufügen:
0 3 * * 1 /home/deinname/headscale/derper/sync-cert.sh >> /var/log/derper-cert-sync.log 2>&1
Testen
curl -I https://vpn.example.com/health
curl -I https://derp.example.com:8443
Beide sollten antworten. Damit steht dein eigener Headscale-Server samt DERP-Relay. Weiter geht's in Teil 2 mit dem Anlegen von Benutzern.
Wichtige Stolpersteine
docker compose restartreicht bei Config-Änderungen nicht. Es startet den Container mit der alten Konfiguration neu. Nutze immerdocker compose up -d --force-recreate <service>.headscale nodesundpreauthkeyserwarten teils die numerische User-ID, teils den Namen. Mitheadscale users listfindest du beides heraus.