Headscale is not a VPN you install on a VPS; it is the coordination server that hands out keys and routes to machines that are already running the real Tailscale client. Get that distinction wrong and the rest of the setup makes no sense — get it right and the whole thing is one small container behind a reverse proxy.Headscale ist kein VPN, das du auf einem VPS installierst; es ist der Koordinationsserver, der Schlüssel und Routen an Maschinen verteilt, auf denen bereits der echte Tailscale-Client läuft. Verwechselst du das, ergibt der Rest des Setups keinen Sinn — verstehst du es richtig, ist das Ganze ein einziger kleiner Container hinter einem Reverse-Proxy.
Tailscale the product is two separate things bolted together: a coordination server that authenticates devices, hands out a private address for each one, and tells every device which other devices exist, and a WireGuard-based data plane that actually moves packets between them. Headscale is an open-source, self-hosted implementation of the first half only. You still run the official tailscale client on every machine — there is no Headscale client, and there never will be one, because the whole point is to be a drop-in replacement for the control server the official client already knows how to talk to.Das Produkt Tailscale besteht aus zwei getrennten Dingen, die zusammengeschraubt sind: einem Koordinationsserver, der Geräte authentifiziert, jedem eine private Adresse zuteilt und jedem Gerät mitteilt, welche anderen Geräte existieren, und einer WireGuard-basierten Datenebene, die die Pakete zwischen ihnen tatsächlich bewegt. Headscale ist eine Open-Source-, selbst gehostete Implementierung nur der ersten Hälfte. Du betreibst weiterhin den offiziellen tailscale-Client auf jeder Maschine — es gibt keinen Headscale-Client, und es wird auch nie einen geben, denn der ganze Sinn der Sache ist, ein direkter Ersatz für den Kontrollserver zu sein, mit dem der offizielle Client bereits zu sprechen weiß.
Once two nodes know about each other, they try to build a direct WireGuard connection, peer to peer, the same way the commercial product does. Where that fails — symmetric NAT, a restrictive firewall — traffic falls back to a DERP relay, still end-to-end encrypted, so the relay operator never sees the contents. Headscale points at Tailscale's own public DERP map by default, so relay fallback works from day one with nothing extra to run. A DERP server of your own is optional, worth it only once you have a specific reason to keep relayed traffic on your own infrastructure.Sobald zwei Knoten voneinander wissen, versuchen sie, eine direkte WireGuard-Verbindung aufzubauen, Peer-to-Peer, genau wie das kommerzielle Produkt es tut. Scheitert das — symmetrisches NAT, eine restriktive Firewall —, fällt der Traffic auf ein DERP-Relay zurück, weiterhin Ende-zu-Ende-verschlüsselt, sodass der Relay-Betreiber den Inhalt nie zu sehen bekommt. Headscale zeigt standardmäßig auf Tailscales eigene öffentliche DERP-Map, sodass der Relay-Fallback von Tag eins an funktioniert, ohne dass du zusätzlich etwas betreiben musst. Ein eigener DERP-Server ist optional und lohnt sich erst, wenn du einen konkreten Grund hast, gerelayten Traffic auf deiner eigenen Infrastruktur zu halten.
The part that is genuinely simple here is the networking the control plane itself needs. Headscale's job is answering HTTPS requests from clients — registrations, key exchanges, the list of peers — so the only inbound port that matters is 443. Everything else is outbound: the container reaching out to fetch the DERP map, clients reaching out to register.Der Teil, der hier wirklich einfach ist, ist das Netzwerk, das die Kontrollebene selbst braucht. Headscales Aufgabe ist es, HTTPS-Anfragen von Clients zu beantworten — Registrierungen, Schlüsselaustausch, die Liste der Peers —, weshalb der einzige eingehende Port, der zählt, 443 ist. Alles andere ist ausgehend: der Container, der die DERP-Map abruft, Clients, die sich registrieren.
That means the same NAT IPv4 catch every other self-hosted web app runs into applies here too: 443 has to be one of the ports your provider actually forwards to your VPS, or clients cannot reach the control server over HTTPS at all — tailscale up --login-server just times out. Whether 443 is forwarded by default depends on the plan, so check before you commit a domain to it. The alternative is a dedicated IPv4 — available on request by e-mail, not as a self-service add-on — which makes any port yours to open.Das heißt, dieselbe NAT-IPv4-Falle, in die jede andere selbst gehostete Web-App läuft, gilt auch hier: 443 muss einer der Ports sein, die dein Provider tatsächlich zu deinem VPS weiterleitet, sonst erreichen Clients den Kontrollserver über HTTPS überhaupt nicht — tailscale up --login-server läuft einfach in ein Timeout. Ob 443 standardmäßig weitergeleitet wird, hängt vom Tarif ab, prüfe das also, bevor du eine Domain darauf festlegst. Die Alternative ist eine dedizierte IPv4 — auf Anfrage per E-Mail verfügbar, nicht als Self-Service-Add-on —, mit der dir jeder Port gehört, den du öffnen willst.
If you later run your own DERP relay instead of leaning on Tailscale's public one, budget for that separately: it needs its own reachable TCP port (443 again on its own hostname, or another forwarded port) plus STUN on 3478/udp, which is how it helps two nodes discover each other's real address for the peer-to-peer attempt. None of that is required just to get Headscale working. Read NAT IPv4 vs a dedicated IP and NAT IPv4, ports and forwarding before you settle on a domain.Betreibst du später ein eigenes DERP-Relay, statt dich auf Tailscales öffentliches zu verlassen, plane das separat ein: Es braucht einen eigenen erreichbaren TCP-Port (wieder 443, auf seinem eigenen Hostnamen, oder ein anderer weitergeleiteter Port) plus STUN auf 3478/udp, womit es zwei Knoten hilft, gegenseitig die echte Adresse für den Peer-to-Peer-Versuch herauszufinden. Nichts davon ist nötig, nur damit Headscale funktioniert. Lies NAT IPv4 vs. dedizierte IP und NAT IPv4, Ports und Weiterleitung, bevor du dich auf eine Domain festlegst.
services:
headscale:
image: headscale/headscale:0.23.0 # check github.com/juanfont/headscale/releases for the current tag
restart: unless-stopped
command: serve
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "127.0.0.1:8080:8080"
volumes: {}
Pin the tag rather than tracking latest — an upgrade should be something you choose and read the changelog for, not something that happens quietly on a restart. Two bind mounts, not named volumes, keep config.yaml and the SQLite database as ordinary files on the host you can inspect and back up without going through Docker at all. The web port stays on 127.0.0.1 because only Caddy, on the same host, ever needs to reach it directly.Pinne den Tag, statt latest zu verfolgen — ein Upgrade sollte etwas sein, das du bewusst wählst und dessen Changelog du liest, nicht etwas, das bei einem Neustart still passiert. Zwei Bind-Mounts statt benannter Volumes halten config.yaml und die SQLite-Datenbank als gewöhnliche Dateien auf dem Host, die du inspizieren und sichern kannst, ganz ohne den Umweg über Docker. Der Web-Port bleibt auf 127.0.0.1, weil nur Caddy, auf demselben Host, ihn je direkt erreichen muss.
Before the first docker compose up, put a real config.yaml in ./config — the image ships no configuration of its own and will not start without one; the next section covers where to get it and what to change.Lege vor dem ersten docker compose up eine echte config.yaml in ./config ab — das Image bringt keine eigene Konfiguration mit und startet ohne eine nicht; der nächste Abschnitt behandelt, woher du sie bekommst und was du änderst.
A handful of settings decide whether this works at all; the rest can stay at whatever the shipped example sets.Eine Handvoll Einstellungen entscheidet, ob das hier überhaupt funktioniert; der Rest kann bleiben, wie es das mitgelieferte Beispiel vorgibt.
server_url — the full public HTTPS URL clients will be told to trust, for example https://headscale.example.com. This has to match the hostname in your Caddy config and your DNS record exactly, protocol included; get it wrong and every client you register will be pointed at the wrong place until you fix it and re-register them.server_url — die vollständige öffentliche HTTPS-URL, der Clients vertrauen sollen, zum Beispiel https://headscale.example.com. Das muss exakt dem Hostnamen in deiner Caddy-Konfiguration und deinem DNS-Eintrag entsprechen, Protokoll eingeschlossen; machst du das falsch, zeigt jeder Client, den du registrierst, auf die falsche Stelle, bis du es korrigierst und ihn neu registrierst.listen_addr — where the process itself binds inside the container, typically 0.0.0.0:8080 to match the compose file's internal port mapping above.listen_addr — woran der Prozess selbst innerhalb des Containers bindet, typischerweise 0.0.0.0:8080, passend zum internen Port-Mapping der Compose-Datei oben.base_domain under the DNS settings gives every node a stable name like laptop.<base_domain> in addition to its private IP, and turning MagicDNS on is what makes those names resolve for connected clients without you running a separate resolver.DNS und MagicDNS — eine base_domain unter den DNS-Einstellungen gibt jedem Knoten zusätzlich zu seiner privaten IP einen festen Namen wie laptop.<base_domain>, und MagicDNS einzuschalten ist es, was diese Namen für verbundene Clients auflösbar macht, ohne dass du einen eigenen Resolver betreibst.config-example.yaml shipped with the tag you actually pinned rather than copy one from an older blog post.IP-Präfixe — der private Adressbereich, aus dem Knoten zugeteilt werden. Das ist einer der Keys, deren genaue Form sich zwischen Headscale-Releases verschoben hat (ältere Versionen nahmen eine flache Liste, aktuelle verschachteln sie unter einem eigenen Block getrennt für v4 und v6), gleiche das also mit der config-example.yaml ab, die mit dem Tag mitgeliefert wird, den du tatsächlich gepinnt hast, statt eine aus einem älteren Blogbeitrag zu kopieren.Download the example configuration for exactly the version you pinned, straight from the project's repository, so the keys match the binary you run:Lade die Beispielkonfiguration für genau die Version, die du gepinnt hast, direkt aus dem Repository des Projekts, damit die Schlüssel zu der Binary passen, die du betreibst:
curl -fsSL -o ./config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/v0.23.0/config-example.yaml
Edit the keys above in that file, then start the stack.Bearbeite die obigen Keys in dieser Datei und starte dann den Stack.
headscale.example.com {
reverse_proxy 127.0.0.1:8080
}
Point the domain's A record at your VPS, let it settle, and only then start Caddy — it requests and renews the certificate for this one hostname automatically the moment it can reach an ACME challenge on 443, and it cannot do that before DNS resolves. This is also why server_url in config.yaml has to be https:// and not http://: the official client expects an HTTPS control server, and over plain HTTP your nodes' control traffic and keys would cross the internet in the clear.Richte den A-Eintrag der Domain auf deinen VPS, lass ihn sich setzen, und starte erst dann Caddy — es fordert das Zertifikat für diesen einen Hostnamen automatisch an und erneuert es, sobald es eine ACME-Challenge auf 443 erreichen kann, und das kann es nicht, bevor DNS auflöst. Das ist auch der Grund, warum server_url in config.yaml https:// sein muss und nicht http://: Der offizielle Client erwartet einen HTTPS-Kontrollserver, und über einfaches HTTP würden der Kontrollverkehr und die Schlüssel deiner Knoten im Klartext durchs Internet gehen.
Connect to the VPS over SSH (connect to your VPS over SSH) if you haven't already, cd into the directory holding your compose file, and bring the stack up:Verbinde dich per SSH mit dem VPS (über SSH mit deinem VPS verbinden), falls noch nicht geschehen, wechsle mit cd in das Verzeichnis mit deiner Compose-Datei, und bring den Stack hoch:
docker compose up -d
docker compose exec headscale headscale users create alice
On the machine you want to connect, install the official Tailscale client the normal way for that platform, then point it at your server instead of Tailscale's own cloud:Installiere auf der Maschine, die du verbinden willst, den offiziellen Tailscale-Client auf die für diese Plattform übliche Weise, und richte ihn dann auf deinen Server statt auf Tailscales eigene Cloud:
sudo tailscale up --login-server https://headscale.example.com
The client prints a link to your own server's /register/ page instead of a Tailscale login, because Headscale has no account system to redirect to. Open that link: the page shows the exact headscale nodes register command with the node's key filled in. Run it on the VPS for the user you just created:Der Client gibt statt eines Tailscale-Logins einen Link auf die /register/-Seite deines eigenen Servers aus, weil Headscale kein Konten-System hat, zu dem es umleiten könnte. Öffne diesen Link: Die Seite zeigt den exakten headscale nodes register-Befehl mit dem eingesetzten Schlüssel des Knotens. Führe ihn auf dem VPS für den Benutzer aus, den du gerade angelegt hast:
docker compose exec headscale headscale nodes register --user alice --key <key printed by the client>
For a headless machine you cannot babysit through that interactive step — a server, a container host, anything unattended — generate a pre-authorization key instead and skip the manual registration entirely:Für eine Headless-Maschine, die du durch diesen interaktiven Schritt nicht begleiten kannst — einen Server, einen Container-Host, alles Unbeaufsichtigte —, erzeuge stattdessen einen Pre-Authorization-Key und überspringe die manuelle Registrierung ganz:
docker compose exec headscale headscale preauthkeys create --user alice --reusable --expiration 24h
sudo tailscale up --login-server https://headscale.example.com --authkey <preauthkey>
headscale nodes list shows every registered node and when it last checked in, which is the fastest way to confirm a registration actually landed.headscale nodes list zeigt jeden registrierten Knoten und wann er sich zuletzt gemeldet hat, was der schnellste Weg ist, zu bestätigen, dass eine Registrierung tatsächlich angekommen ist.
Headscale itself is a single Go process holding a SQLite file open and answering short HTTPS requests; it never sits in the data path. Idle memory use is measured in tens of MiB and stays there, because control traffic — registrations, periodic key refreshes, the peer list — is tiny next to the WireGuard traffic actually moving between your nodes, which never touches this VPS unless it has to be relayed. A Starter box is genuinely plenty for the control plane plus Caddy, with headroom to spare.Headscale selbst ist ein einzelner Go-Prozess, der eine SQLite-Datei offen hält und kurze HTTPS-Anfragen beantwortet; es sitzt nie im Datenpfad. Der Speicherverbrauch im Leerlauf bewegt sich im zweistelligen MiB-Bereich und bleibt dort, weil der Kontroll-Traffic — Registrierungen, periodische Schlüsselerneuerungen, die Peer-Liste — winzig ist im Vergleich zum WireGuard-Traffic, der tatsächlich zwischen deinen Knoten bewegt wird und diesen VPS nie berührt, außer er muss gerelayt werden. Eine Starter-Maschine reicht für die Kontrollebene plus Caddy wirklich locker, mit Spielraum übrig.
The one thing that changes this math is running your own DERP relay on the same box with real, sustained relayed traffic — at that point it is doing data-plane work, and sizing should follow expected relay throughput instead.Das Einzige, was diese Rechnung ändert, ist, ein eigenes DERP-Relay mit echtem, dauerhaftem gerelaytem Traffic auf derselben Maschine zu betreiben — an diesem Punkt leistet sie Datenebenen-Arbeit, und die Dimensionierung sollte sich stattdessen am erwarteten Relay-Durchsatz orientieren.
Two things need to survive a disaster: the ./config directory (config.yaml and the private keys Headscale generated on first boot) and the SQLite database under ./data. Because both are plain bind-mounted directories, a straightforward file-level backup off the VPS covers them — there is no built-in dump command to reach for here. For a database copy you can take while the container keeps running, use SQLite's own online backup rather than copying the file mid-write:Zwei Dinge müssen einen Ausfall überleben: das Verzeichnis ./config (config.yaml und die privaten Schlüssel, die Headscale beim ersten Boot erzeugt hat) und die SQLite-Datenbank unter ./data. Weil beides schlichte, bind-gemountete Verzeichnisse sind, deckt sie ein einfaches dateibasiertes Backup abseits des VPS ab — es gibt hier keinen eingebauten Dump-Befehl, zu dem du greifen könntest. Für eine Datenbankkopie, die du bei laufendem Container ziehen kannst, nutze SQLites eigenes Online-Backup, statt die Datei mitten im Schreibvorgang zu kopieren:
sqlite3 ./data/db.sqlite ".backup './data/db-backup.sqlite'"
Copy that backup file and the whole ./config directory off the machine; a copy sitting on the same disk as the original is not a backup. back up your VPS covers what off-machine actually means in practice.Kopiere diese Backup-Datei und das gesamte Verzeichnis ./config von der Maschine herunter; eine Kopie, die auf derselben Festplatte wie das Original liegt, ist kein Backup. deinen VPS sichern behandelt, was „abseits der Maschine“ in der Praxis tatsächlich bedeutet.
Updates are a pull and a restart, same as any compose stack:Updates sind ein Pull und ein Neustart, wie bei jedem Compose-Stack:
docker compose pull
docker compose up -d
Read the release notes for whatever version you are jumping to before you do it, and take a fresh backup immediately beforehand — Headscale migrates its database automatically on the first start after an upgrade, and a migration only feels safe when the backup behind it is minutes old, not months.Lies die Release Notes der Version, auf die du springst, bevor du es tust, und mach unmittelbar davor ein frisches Backup — Headscale migriert seine Datenbank beim ersten Start nach einem Upgrade automatisch, und eine Migration fühlt sich nur dann sicher an, wenn das Backup dahinter Minuten alt ist, nicht Monate.
Full disclosure: this is what we sell. The control plane idles in tens of MiB, so a 1 vCPU, 1 GiB Starter runs it comfortably; only move up if the same box also becomes your DERP relay for real, sustained traffic.Zur vollen Transparenz: Das ist, was wir verkaufen. Die Kontrollebene läuft im Leerlauf im zweistelligen MiB-Bereich, sodass ein 1-vCPU-1-GiB-Starter sie bequem betreibt; steige erst höher, wenn dieselbe Maschine zusätzlich zu deinem DERP-Relay für echten, dauerhaften Traffic wird.
Linux KVM VPS — EUR 4.99 to EUR 59.99 a month, on our own single-tenant bare metal in Dallas, TX and Charlotte, NC. Full hardware virtualisation (KVM), your own kernel, full root. Six tiers, vps-starter to vps-ultra. Starter is 1 vCPU, 1 GiB RAM, 25 GB disk.Linux-KVM-VPS — 4,99 bis 59,99 EUR im Monat, auf unserer eigenen Single-Tenant-Bare-Metal-Hardware in Dallas, TX und Charlotte, NC. Vollständige Hardware-Virtualisierung (KVM), eigener Kernel, volles Root. Sechs Tarife, vps-starter bis vps-ultra. Starter hat 1 vCPU, 1 GiB RAM, 25 GB Speicher.
You order in the shop, pay by card (Stripe) or SEPA bank transfer, and your login details are e-mailed to you once the service is set up. Support is e-mail, run by one person, with no guaranteed response time. All prices are final totals under the German small-business rule (§19 UStG); no VAT is added or shown.Du bestellst im Shop, zahlst per Karte (Stripe) oder SEPA-Überweisung, und deine Zugangsdaten werden dir per E-Mail zugeschickt, sobald der Dienst eingerichtet ist. Support läuft per E-Mail, von einer einzelnen Person betrieben, ohne garantierte Reaktionszeit. Alle Preise sind Endpreise. Gemäß § 19 UStG wird keine Umsatzsteuer ausgewiesen.
Order vps-starter → · Linux KVM VPS overviewvps-starter bestellen → · Übersicht Linux-KVM-VPS
Written by the person who runs overnight.host: a small, honest hosting company on dedicated bare metal — Linux VPS, game servers, web hosting. Live status at up.overnight.host.Geschrieben von der Person, die overnight.host betreibt: ein kleines, ehrliches Hosting-Unternehmen auf dedizierter Bare-Metal-Hardware — Linux-VPS, Gameserver, Webhosting. Live-Status unter up.overnight.host.
Yes, on every device. Headscale only replaces the coordination server; the official Tailscale client is still what builds the WireGuard tunnel and moves your actual traffic. Point it at your own server with --login-server instead of leaving it on Tailscale's default.Ja, auf jedem Gerät. Headscale ersetzt nur den Koordinationsserver; der offizielle Tailscale-Client ist weiterhin das, was den WireGuard-Tunnel aufbaut und deinen tatsächlichen Traffic bewegt. Richte ihn mit --login-server auf deinen eigenen Server, statt ihn auf Tailscales Standard zu belassen.
Only when it has to. Two nodes that can reach each other directly build a peer-to-peer WireGuard connection and your Headscale server never sees that traffic at all. It only gets relayed through a DERP server, encrypted, when direct connection fails — and by default that relay is Tailscale's own public DERP network, not your VPS, unless you deliberately set up your own.Nur, wenn es sein muss. Zwei Knoten, die sich direkt erreichen können, bauen eine Peer-to-Peer-WireGuard-Verbindung auf, und dein Headscale-Server bekommt diesen Traffic überhaupt nie zu sehen. Er wird nur dann, verschlüsselt, über einen DERP-Server gerelayt, wenn die direkte Verbindung scheitert — und standardmäßig ist dieses Relay Tailscales eigenes öffentliches DERP-Netzwerk, nicht dein VPS, sofern du nicht bewusst ein eigenes aufsetzt.
No. NAT IPv4 is enough as long as 443 is one of the ports your provider forwards to your VPS, since that is the only inbound port the control plane needs. A dedicated IPv4 only matters if 443 is not forwarded on your plan or you want a port of your choosing; on our plans a dedicated IPv4 is available on request by e-mail, not as a self-service add-on.Nein. NAT-IPv4 reicht, solange 443 einer der Ports ist, die dein Provider zu deinem VPS weiterleitet, denn das ist der einzige eingehende Port, den die Kontrollebene braucht. Eine dedizierte IPv4 wird erst wichtig, wenn 443 in deinem Tarif nicht weitergeleitet wird oder du einen Port deiner Wahl willst; bei unseren Tarifen ist eine dedizierte IPv4 auf Anfrage per E-Mail verfügbar, nicht als Self-Service-Add-on.
server_url is wrong?Was geht tatsächlich kaputt, wenn server_url falsch ist?Every client bakes that URL in as the address it trusts for future coordination. Get it wrong — http instead of https, a typo in the hostname — and clients fail to connect or reach the wrong endpoint; fix the config and re-run tailscale up --login-server on each affected machine.Jeder Client brennt diese URL als die Adresse ein, der er für künftige Koordination vertraut. Ist sie falsch — http statt https, ein Tippfehler im Hostnamen —, scheitern Clients an der Verbindung oder erreichen den falschen Endpunkt; korrigiere die Konfiguration und führe tailscale up --login-server auf jeder betroffenen Maschine erneut aus.
Small. It is one SQLite file holding users, node registrations and preauthkeys — there is no traffic log or packet history stored, since Headscale never sees the actual WireGuard payloads. Back it up the same way you would any small SQLite database: a .backup copy taken while the service runs, moved off the VPS on a schedule.Klein. Es ist eine einzige SQLite-Datei mit Benutzern, Knoten-Registrierungen und Preauth-Keys — es gibt kein Traffic-Log und keine Paket-Historie, da Headscale nie die tatsächlichen WireGuard-Payloads zu sehen bekommt. Sichere sie genauso wie jede kleine SQLite-Datenbank: eine .backup-Kopie, während der Dienst läuft, regelmäßig vom VPS heruntergeladen.
Prices are final totals; no VAT is shown (§19 UStG). Need something the shop does not list? Email us for a written offer.Alle Preise sind Endpreise ohne ausgewiesene USt. (§19 UStG). Du brauchst etwas, das nicht im Shop steht? Schreib uns für ein schriftliches Angebot.
Order now →Jetzt bestellen → Request a custom configIndividuelle Konfiguration anfragen