The NordBastion polar-bear mascot standing in a dark Nordic stone hall with one paw on a carved runic keystone obelisk, a floating cyan holographic ring of keys hovering above it, and five glowing device glyphs — laptop, phone, tablet, server tower and globe — linked directly to one another in a bright cyan mesh overhead while faint dotted key lines fall from the ring to each device, a server rack and an aurora-lit arched window behind him
How-to · Private networking·19 min read · 50 min hands-on

Your own control server.
The map is the thing worth holding.

A mesh VPN needs somebody to hand out the keys. With Tailscale that somebody is a company; with plain WireGuard it is you, by hand, forever. Headscale is the third answer: the same open-source client, talking to a coordination server on a box you rent anonymously. Six steps to a tailnet whose device map, access rules and fallback relays are all yours — Headscale behind Caddy, ACLs written down, MagicDNS pointed at your own resolver, a subnet router, an exit node and an embedded DERP. The box costs $3.90 a month. Tested on Debian 12.

The six steps
  1. 01

    Choose

    WireGuard, Tailscale, Headscale

  2. 02

    Provision

    Three names fixed before boot

  3. 03

    Deploy

    Headscale, Caddy, one config

  4. 04

    Enrol

    Users, pre-auth keys, clients

  5. 05

    Route

    Subnets, exit node, ACLs

  6. 06

    Keep

    DERP, backups, upgrades

Before you start · What a control plane is

Nobody can read the traffic. Somebody still holds the map.

WireGuard is deliberately incomplete. It is a small, fast, auditable tunnel between two public keys, and it has no opinion whatsoever about how those keys reach each other. That omission is why the protocol is as good as it is — and it is also why a WireGuard network of fifteen devices is a spreadsheet. Every peer needs every other peer's public key and current address, and every laptop that moves between an office, a train and a hotel invalidates part of that spreadsheet.

A mesh VPN is the answer to that specific problem, and its shape is worth stating precisely, because it decides what you are trusting. The data plane stays exactly as it was: each device generates its own key pair locally, keeps the private half, and encrypts directly to its peers. On top of it sits a control plane — a small service that collects public keys and endpoints, hands each device the list of peers it is allowed to talk to, and helps two devices behind hostile routers find each other. That service never holds a private key and never sees a plaintext byte.

What it does hold is the map, and the map is more interesting than most people assume. Every device name and every user name. Every public key. The public address each device is dialling from right now, which is a live record of where your laptop and your phone physically are. The times each device connects and disconnects, which describes your day. And the policy that decides who may reach what, which describes your organisation. None of that requires bad faith from anyone; it is simply what a coordination server is for.

There is one further property that matters more than the metadata. The control plane is the thing that distributes keys, so it is also the thing that could add one. A control server that is compromised, or compelled, can tell your devices that a new machine is a legitimate member of the network — and your devices, doing exactly what they were built to do, will encrypt to it. Tailscale takes that seriously enough to have built a signing feature against it. Self-hosting solves the same problem from the other direction: the party that could do it is you. Four arrangements, and what each one actually gives away:

Property Manual WireGuard Tailscale, hosted Headscale on your VPS Headscale + your own DERP
Who can read the payloadNobody but the peersNobody but the peersNobody but the peersNobody but the peers
Who holds the device mapYou, in a text fileA company, in its jurisdictionYou, on your VPSYou, on your VPS
Who sees where devices connect fromOnly the peers themselvesThe coordination server, continuouslyYour server, continuouslyYour server, continuously
Who relays when a direct link failsNobody — the link simply failsThe provider's relay fleetStill the provider's relays, by defaultYour relay, in your datacentre
Adding the twentieth deviceNineteen config editsOne commandOne commandOne command
Pick it whenFew devices, none of them roamYou want zero operations and accept the mapYou want the map and can run one small boxYou want the map and the fallback path too

The first column is not a straw man. For a static handful of servers, hand-written WireGuard is genuinely the better engineering answer, and it is one fewer service to patch. This guide is about the point where the spreadsheet stops being funny.

Step 01 · Choose

Do you need a control plane? Count the devices that move.

There are three honest answers to "I want my machines to reach each other privately", and only one of them is this guide. Plain WireGuard is the smallest: a kernel module, one config file per peer, no daemon beyond the interface, nothing to keep current except the operating system. If your devices are servers with fixed addresses and you can count them on one hand, stop here and read the WireGuard guide instead. You will end up with less software and a shorter list of things that can break.

Tailscale is the largest: the same WireGuard underneath, plus a coordination service, NAT traversal that works from behind almost anything, a relay fleet on several continents, a web console, single sign-on and a support contract. It is genuinely excellent software and the free tier covers a household comfortably. The price is not money — it is that the map of every device you own, and the record of where each one connects from, lives in a company's database.

Headscale is the third answer, and it is narrower than either: an open-source reimplementation of the coordination server, run by you, spoken to by the same official client. You keep the automation and the NAT traversal; you take on a small service to operate, a partial feature set, and upgrades that need reading. Nobody should pretend that is free. It buys exactly one thing, and it is the thing this site cares about: nobody else holds the map.

The five-device threshold

Manual WireGuard configuration grows with the square of the fleet: four peers is six links, ten peers is forty-five, twenty peers is a hundred and ninety. Add a roaming laptop, whose endpoint changes with every network it joins, and the maintenance stops being occasional. In practice the crossover sits around five devices, or at the first device that moves daily, or at the first access rule you cannot express as "everyone can reach everything".

One more thing to decide now rather than later, because it changes what you install: whether this network is only for you, or whether other people will join it. A tailnet for one person can stay wide open internally and nothing bad happens. A tailnet with a colleague, a contractor or a family member on it needs written access rules from the first week — step 05 — and it needs users to be separate rather than everyone sharing a login. Deciding that up front costs a paragraph; retrofitting it costs an afternoon.

Step 02 · Provision

Three names, decided before the first boot. All three are hard to change.

Order the box first. In the panel: Order → VPS → Sentinel, image Debian 12, in whichever bastion you want the map to live in — Helsinki, Stockholm, Oslo or Reykjavík. No email address is required to open the account and the invoice settles in Monero, Bitcoin, Lightning or any other supported asset. Pick the bastion closest to the majority of your devices if you also intend to run the relay from step 06; latency to a relay is latency on every connection it carries.

Then run the first-hour hardening checklist — key-only SSH on a moved port, unattended upgrades, a default-deny firewall. Everything below assumes a box that is not already somebody else's, and this one is worth more than most: it is the machine that decides which keys are trusted on every device you own.

Decide these three before anything registers

Every device bakes all three into its state the moment it joins, so changing one afterwards means re-enrolling the fleet by hand. One: the control server URL — the exact public HTTPS address every client will dial, scheme and hostname included. Two: the tailnet DNS suffix, the domain that MagicDNS will answer for internally; it must be one you control and it must not be a parent or a child of the control server's own hostname, which is the single most common first-boot failure. Three: the address ranges the tailnet hands out, conventionally the 100.64.0.0/10 carrier-grade NAT block for IPv4 and a unique-local /48 for IPv6; leave the defaults unless they already collide with a network you use.

A worked example, and the shape to copy: the control server answers on vpn.example.com, the tailnet suffix is ts.example.com, and the ranges stay at their defaults. Notice that the suffix is a sibling of the control hostname rather than its parent — vpn.example.com and ts.example.com share a registrable domain but neither contains the other. Point an A record — and an AAAA if you want it reachable over IPv6 — at the box, and let it propagate before you start, because the reverse proxy will try to obtain a certificate for that name the first time it boots.

Then write the firewall rules while nothing is listening yet. The control server is a public HTTPS endpoint by necessity: every device, on every network, has to be able to reach it. The UDP port is only needed if you intend to run your own relay in step 06, and it is easier to open it now than to debug its absence later.

ufw default deny incoming
ufw allow 47222/tcp                  # administrative SSH, moved
ufw allow 80/tcp                     # ACME HTTP-01 challenge
ufw allow 443/tcp                    # the control server, and DERP over HTTPS
ufw allow 3478/udp                   # STUN, only if you run your own relay
ufw enable
ufw status numbered

Install Docker from the official repository rather than the distribution package, so Compose v2 is present and current. Then create the two directories that will hold everything worth backing up — one for configuration, one for state — and resist the urge to put anything else in them.

curl -fsSL https://get.docker.com | sh
docker compose version

install -d -m 0750 /srv/headscale/config
install -d -m 0750 /srv/headscale/data
cd /srv/headscale
Step 03 · Deploy

Two containers and one file. Pin the version, always.

Headscale and Caddy, and nothing else. Headscale listens on plain HTTP inside the Docker network; Caddy terminates TLS, obtains the certificate on first boot and renews it without being asked. The metrics listener stays on localhost, because it is an internal debug surface and not a public one.

One rule before the file: pin the image to an exact version. Headscale is pre-1.0 software that has renamed configuration keys and CLI subcommands across minor releases more than once. A floating tag turns a routine container restart into an outage on a service whose job is to let you reach everything else.

/srv/headscale/docker-compose.yml

services:
  headscale:
    image: docker.io/headscale/headscale:0.26            # pin it; never :latest
    container_name: headscale
    command: serve
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    ports:
      - "3478:3478/udp"                                  # STUN, for your own DERP
    restart: unless-stopped

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
    restart: unless-stopped

volumes:
  caddy_data:

/srv/headscale/Caddyfile

vpn.example.com {
    reverse_proxy headscale:8080
}

That is the whole proxy configuration. Caddy speaks HTTP/2 and upgrades long-lived connections without being told to, which matters here because every client holds one open to receive map updates. Take the sample configuration file that ships with the version you pinned and edit it rather than writing one from scratch — the file is long, most of it is sensible defaults, and the keys that matter are these:

/srv/headscale/config/config.yaml — the lines that matter

server_url: https://vpn.example.com          # exactly what clients will dial
listen_addr: 0.0.0.0:8080                    # inside the Docker network only
metrics_listen_addr: 127.0.0.1:9090          # debug surface, keep it local

prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48

noise:
  private_key_path: /var/lib/headscale/noise_private.key

database:
  type: sqlite                               # postgres for a large fleet
  sqlite:
    path: /var/lib/headscale/db.sqlite

dns:
  magic_dns: true
  base_domain: ts.example.com                # NOT a parent or child of server_url
  nameservers:
    global:
      - 1.1.1.1                              # replaced by your own in step 05

Bring it up, then read the log rather than assuming. A first boot that cannot obtain a certificate, or that rejects the DNS suffix because it overlaps the server hostname, says so clearly and then stops.

docker compose up -d
docker compose logs -f headscale

# From your laptop — a control server that is up answers this:
curl -s https://vpn.example.com/health

First boot generates the private key material in /srv/headscale/data. Copy it somewhere safe now, before a single device registers, and again after the first enrolment: losing that key means every device rejects the server it has been trusting, and the only repair is re-enrolling all of them by hand. Step 06 turns that copy into a real backup.

Step 04 · Enrol

A user, a key, a client. Then check whether it went direct.

The headscale command lives inside the container, so every administrative action is a docker exec. Create a user first — a user is an owner of devices and the unit that access rules refer to, so give it the name of a person or a role rather than a machine.

hs() { docker exec -it headscale headscale "$@"; }

hs users create alice
hs users list                      # note the numeric id — newer releases want it

# A single-use key that expires in an hour is the right default.
hs preauthkeys create --user 1 --expiration 1h

Then bring up the official client on the device, pointing it at your server instead of the default one. This is a supported flag, not a workaround — the client is open source and the login server is a parameter.

# On the device (Debian/Ubuntu shown; every platform has an equivalent):
curl -fsSL https://tailscale.com/install.sh | sh

sudo tailscale up \
  --login-server https://vpn.example.com \
  --auth-key <the-key-you-just-created>

tailscale status
tailscale ip -4

Without a pre-authentication key the client prints a URL instead; visiting it shows a registration command to run on the server, which is the interactive path and is handy for a device you are sitting in front of. Either way, do the second device before you celebrate — a mesh of one proves nothing. Ping it by its tailnet address, then read the status output carefully:

hs nodes list

# On a client, the word after the peer name is the one that matters:
tailscale status
#   100.64.0.2  laptop  alice  linux  active; direct 203.0.113.9:41641
#                                              ^^^^^^ direct, not relay

tailscale netcheck                 # why it chose what it chose

direct means the two devices found each other and the traffic is peer-to-peer. relay means it is going through a DERP server, which works fine but adds latency and puts a third machine on the path — and by default that machine is not yours. Step 06 is about fixing which one it is.

Phones, tablets and the Apple detour

Desktops take a command-line flag. Android exposes the custom server in the app, behind the version number in its settings screen. Apple platforms have no in-app field: you install a small configuration profile that sets the control server URL before signing in, which is tedious once and permanent afterwards. Two things save time. Check the Headscale documentation's per-client page rather than guessing, because the Apple flow has changed more than once. And log the app out of Tailscale's own service first — an app that already holds a session there will often refuse a different server until it has been signed out, or reinstalled.

Two habits worth adopting immediately. Use single-use, short-lived pre-authentication keys for people, and reusable or ephemeral ones only for automation that genuinely needs them — an ephemeral key registers a device that removes itself when it goes offline, which is exactly right for a CI runner and exactly wrong for a laptop. And decide your position on key expiry now: a registered device carries an expiry date, and a device whose key has expired stops being reachable until somebody re-authenticates it. That is a security feature on a laptop and an outage generator on a headless server at three in the morning. Check how your version manages expiry — the subcommands have moved — and set it deliberately per device rather than discovering the default the hard way.

Step 05 · Route

Advertised is not approved. Two commands, two machines.

A tailnet where every device reaches every other device is already useful. Three additions make it a network rather than a set of tunnels, and all three share one property that trips people up: the client advertises a capability, and the server must approve it. A route that is advertised but not approved simply does not exist, silently, and the symptom is that you can ping the router but nothing behind it.

A subnet router — put a whole LAN on the tailnet. One machine on a home or office network advertises the network's range; every other device then reaches the printers, the NAS and the management interfaces behind it without anything else being installed there. Turn on forwarding in the kernel first, or the packets stop at the router without an error message.

# On the machine that sits inside the LAN:
echo 'net.ipv4.ip_forward = 1'          | sudo tee /etc/sysctl.d/99-ts.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-ts.conf
sudo sysctl -p /etc/sysctl.d/99-ts.conf

sudo tailscale up --login-server https://vpn.example.com \
  --advertise-routes=192.168.1.0/24 --accept-routes

# On the control server — approve it, or it does not exist.
# The subcommand moved between releases; check `hs nodes --help` first.
hs nodes list
hs nodes approve-routes --identifier 3 --routes 192.168.1.0/24

An exit node — send everything out through the VPS. The same mechanism with a wider route: a device that advertises itself as an exit node offers to carry another device's entire internet connection. On an untrusted café or hotel network that is exactly what you want, and it is the closest thing in this guide to a personal VPN.

# On the VPS that will do the exiting (forwarding must be on, as above):
sudo tailscale up --login-server https://vpn.example.com --advertise-exit-node

# Approve it on the control server, then on the laptop:
tailscale set --exit-node=100.64.0.1 --exit-node-allow-lan-access
tailscale set --exit-node=                    # turn it off again

An exit node is not a commercial VPN and should not be sold to yourself as one. Its address is yours alone and it is stable, which is excellent against the café Wi-Fi and useless against long-term correlation across sites — the trade-off is spelled out in the pillar guide. Note also that you can run the exit node on a second, cheaper VPS and keep the control server free of forwarded traffic entirely.

Access rules — write them before the second person joins. The default policy is allow-everything, which is correct for a tailnet of one and wrong the moment somebody else is on it. The policy is a small HuJSON document: groups of users, tags for machines, and a list of rules that say which sources may reach which destinations on which ports. Point the configuration at the file, keep the file in version control, and reload after each edit.

{
  "groups": {
    "group:admin":     ["alice"],
    "group:contractor":["bob"]
  },
  "tagOwners": {
    "tag:server": ["group:admin"],
    "tag:exit":   ["group:admin"]
  },
  "acls": [
    // Admins reach everything.
    { "action": "accept", "src": ["group:admin"], "dst": ["*:*"] },

    // The contractor reaches one machine, on two ports, and nothing else.
    { "action": "accept", "src": ["group:contractor"],
      "dst": ["tag:server:22,443"] }
  ],
  "autoApprovers": {
    "routes":   { "192.168.1.0/24": ["tag:server"] },
    "exitNode": ["tag:exit"]
  }
}

Two notes on that file. The policy language is Tailscale's, and Headscale implements a large and growing subset of it rather than all of it — check the documentation for your version before relying on an exotic rule. And where the policy lives has moved: older releases named a path in the top-level configuration, newer ones group it under a policy section with a mode that can also keep the rules in the database. Both are documented; the one that matters is the one your pinned version ships.

MagicDNS — short names, and your own resolver everywhere. With MagicDNS on, every device answers to a short name inside the suffix you chose in step 02, so ssh nas works from anywhere without a hosts file. The second half is the interesting one: the control server also pushes a nameserver list to every client, which means the resolver you built in the private DNS resolver guide can serve your phone on mobile data — filtering, caching and logging nothing — while listening on a tailnet address that is not reachable from the internet at all.

dns:
  magic_dns: true
  base_domain: ts.example.com
  override_local_dns: true          # beat whatever DHCP handed the device
  nameservers:
    global:
      - 100.64.0.5                  # your resolver, on the tailnet
Step 06 · Keep

Your own relay, your own backup. And upgrades read before applied.

The relay you forgot you were using. When two devices cannot open a direct connection — symmetric NAT at both ends, a firewall that eats UDP, a carrier doing something creative — the traffic falls back to a DERP relay. It stays encrypted end to end, so the relay carries ciphertext. What the relay sees is who is talking to whom, how much, and when. Out of the box, Headscale hands your clients the public relay map, and those relays are operated by the company you were moving away from. Headscale ships an embedded relay; turning it on puts that fallback in the same Nordic datacentre as the rest of this.

derp:
  server:
    enabled: true
    region_id: 999                  # custom regions live in the 900s
    region_code: "nb-hel"
    region_name: "Helsinki (self-hosted)"
    stun_listen_addr: "0.0.0.0:3478"
  urls:
    - https://controlplane.tailscale.com/derpmap/default   # remove for yours only
  auto_update_enabled: true
  update_frequency: 24h

Leaving the public map alongside your own region is the pragmatic default: clients prefer whichever relay is closest, so a device in the same hemisphere gets yours and a device on the far side of the world is not stranded behind a single Nordic hop. Removing the public URL makes the tailnet strictly self-contained, at the price of worse fallback for distant devices. Both are defensible; pick knowingly rather than by omission.

Four files, offsite, encrypted. The backup is small enough to be embarrassing if it is missing: the database, the private key material, the configuration file and the policy file. Together they are a rounding error on any storage bill, and without the key material every device on the network rejects the server. Send them somewhere that is not this box — ideally a second jurisdiction, which is what the backups guide covers — and restore them onto a scratch VPS once, deliberately, before you need to.

# Stop briefly for a consistent SQLite copy, or use .backup on a live db.
docker compose stop headscale
tar czf /tmp/headscale-$(date +%F).tar.gz -C /srv/headscale config data
docker compose start headscale

restic -r sftp:backup@backup.example.com:/srv/restic backup \
       /tmp/headscale-$(date +%F).tar.gz

Upgrades, which are the actual risk. This is pre-1.0 software and it behaves like it: configuration keys have been renamed, CLI subcommands have moved, and a release has more than once needed a documented migration rather than a restart. None of that is a reason to avoid it — it is a reason to have a routine. Read the release notes before pulling. Take the archive above first. Diff your configuration file against the example shipped with the new version, because that is where renamed keys announce themselves. Bump one minor version at a time rather than three. And keep the old image tag written down, because a rollback you can perform in thirty seconds is what makes the whole thing low-stakes.

Then set a monthly reminder to look at two things: the list of registered devices, so machines you decommissioned six months ago stop being members, and the expiry dates, so nothing silently drops off at a bad moment. A mesh VPN is one of the few services that fails quietly — everything keeps working until the one time you needed the device that was quietly expired.

The honest part · What this does not do

You took back the map. You did not become invisible.

Self-hosting the control plane is a real, specific improvement, and it is not a disappearing act. Five things stay true after everything above is done, and knowing them is the difference between a defence and a comforting story.

The control server is a public, named endpoint. Every device dials it, so it has a hostname and that hostname is published in certificate-transparency logs the moment the certificate is issued. It is discoverable even if you never link to it, and its address is the one constant that all your devices have in common. Use a subdomain of a name you already publish rather than one that describes what it is.

Headscale is a reimplementation, and it says so. It is not built or supported by Tailscale, feature parity is partial by construction, and some of what the hosted product sells — the web console, the public relay fleet, the polished single sign-on paths — either does not exist here or is a third-party project nobody audited. Read the project documentation for whether a feature exists before you build a plan on it, and treat upgrades as a task rather than a background process.

The default relays are still somebody else's. Until you enable the embedded DERP server, a connection that cannot go direct falls back to the public relay fleet. The payload is still end-to-end encrypted and nobody can read it — but the fact that two of your devices are talking, and how much, transits infrastructure you do not own. That is the part of the trust chain most people miss on the first afternoon.

An exit node is a stable address, not a crowd. Routing your traffic through your own VPS defeats the café network and the ISP profile. It does not defeat correlation, because that address belongs to you alone and does not change. A commercial VPN's shared address is worse in every way except the one that matters for that particular threat.

The machine is still in a building. A rented server runs on hardware in a datacentre with doors and staff. What changes is that there is no product-level index of your devices, no company database recording where each one connects from, and no privacy policy that can be amended next quarter. What does not change is that the disk is physical and it is not in your house.

What you have actually bought, precisely: the list of every machine you own, the record of where each one connects from, and the authority to add a new one, are all held on a box you rent under no particular name. That is a narrow claim and a real one. It is worth what it costs only if step 06 is done — a self-hosted control plane without a tested restore is not a privacy improvement, it is a single point of failure with better branding.

Sizing · What to order

The control plane is free. The traffic is what you pay for.

Tier Monthly Resources What it comfortably runs
Sentinel$3.902 vCPU · 4 GB · 120 GB · 1 GbpsThe control server, a relay and light exit-node use. The right start.
A second Sentinel$3.90The exit node, on its own boxKeeps forwarded traffic away from the control plane
Ravelin$16.908 vCPU · 16 GB · 480 GB · 2.5 GbpsA daily-driver exit node for a household, at full line rate
Bulwark$32.9016 vCPU · 32 GB · 960 GB · 5 GbpsA company exit node and a busy relay, together

Nothing in this table is sized for coordination. The control server holds one idle connection per device and a database measured in kilobytes; a few hundred devices will not trouble the smallest tier. What consumes a box is forwarded traffic — relayed connections and, far more, an exit node carrying somebody's whole internet connection. Size for the uplink and the encryption throughput, and remember that bandwidth is unlimited on every tier, so the only question is how fast you want it to be.

Field notes · Six traps

Six ways this goes wrong. Two of them look like DNS.

Trap 01 · Confusing

The tailnet suffix overlaps the server hostname

Choosing a MagicDNS suffix that is a parent or a child of the control server's own hostname breaks name resolution in a way that reads like a DNS outage. Keep them siblings: vpn.example.com for the server, ts.example.com for the tailnet.

Trap 02 · Silent

A route advertised and never approved

The client happily reports that it is advertising 192.168.1.0/24 and nothing behind the router answers, because the server never approved it. There is no error anywhere. Approve on the server, then check the route list rather than the client.

Trap 03 · Slow

Everything relayed, nothing direct

A network that blocks outbound UDP forces every connection through a relay: it works, it is slower, and it puts a third machine on the path. tailscale status says relay instead of direct, and netcheck says why. Fix the firewall or accept the hop knowingly.

Trap 04 · Exposed

The default allow-everything policy, with a guest on the tailnet

A fresh tailnet lets every device reach every other device on every port. That is correct for one person and wrong the minute a contractor's laptop joins — including the management interfaces they never knew were there. Write the policy before the invitation.

Trap 05 · Fatal

The private key that was never backed up

Rebuild the box, restore the database, forget the key material — and every device refuses the server it trusted yesterday. There is no clever recovery; there is a day of re-enrolling phones by hand. Copy the data directory before the first device joins.

Trap 06 · Avoidable

A floating image tag on a pre-1.0 service

Configuration keys and CLI subcommands have been renamed across minor releases. With :latest, an unrelated restart pulls a new version and the control plane for your entire fleet stops. Pin the tag, read the notes, upgrade on purpose.

FAQ · Self-hosted mesh VPN

Questions, answered.

Eleven questions that come up while building this — starting with the two that decide whether you should build it at all.

Is Headscale an official Tailscale product?

No, and it is worth being precise about what that means. Headscale is an independent, open-source reimplementation of the coordination server — the piece of Tailscale that distributes public keys, endpoints and policy. It is not built, sold or supported by Tailscale, it carries no commercial support contract, and if it breaks you are reading the issue tracker rather than opening a ticket. What it does do is speak the protocol that the official, open-source Tailscale client already speaks, which is why the client works unmodified against it. Two practical consequences follow. Feature parity is partial and always will be, because the project is chasing a moving target maintained by a much larger team — check the project's own documentation for whether a feature exists before you design around it. And upgrades deserve more care than the average container bump: the configuration schema and several CLI subcommands have been renamed across minor releases, which is the single most common way people break a working install.

Can the coordination server read my traffic?

No — and this is the part of the architecture worth understanding rather than taking on faith. Every device generates its own WireGuard key pair locally and the private half never leaves it. What a device uploads to the control server is its public key, its current endpoints and some metadata; what it downloads is the same for every other device it is allowed to reach. The tunnels are then negotiated directly between the devices, and the encryption is ordinary WireGuard between two keys the server has never held. So the control server cannot decrypt anything, and that is true of Tailscale's server too. What it can do is different and less discussed: it holds the map. Device names, user names, public keys, the public IP each device is currently dialling from, which device woke up at 3am, and the policy that says who may reach whom. It is also the thing that distributes keys, so a control server that is compromised or compelled can add a device to your network. Self-hosting does not fix a maths problem; it moves an administrative one onto a box you own.

What happens to my network if the control server goes down?

Much less than people expect, which is the reassuring part of this design. Devices cache the network map they were last given, and existing WireGuard tunnels are peer-to-peer — they do not route through the control server and do not need it to keep working. A reboot of the VPS during a normal working day is usually invisible. What stops is change: no new device can register, a device that moves to a new network may take longer to find its peers again, policy edits do not propagate, and any node whose key expires while the server is down will not be able to renew it. So the correct way to think about the control server is as a control plane rather than a data path: an outage is an inconvenience, not a disconnection. That said, treat it as production. Back it up (step 06), and if a whole department depends on the tailnet, put the control server on its own small box rather than sharing it with something that gets restarted often.

Headscale or plain WireGuard — when is the control plane actually worth it?

Count your devices and count how many of them move. Plain WireGuard is a superb answer for a small, static set: three or four peers, fixed endpoints, one config file each, nothing to run except the kernel module. It is less software, less attack surface and less maintenance than anything in this guide, and our WireGuard guide covers it end to end. The cost is that WireGuard deliberately has no key distribution, so every new device is an edit on every other device — a job that grows as the square of your fleet — and roaming clients on hotel Wi-Fi and mobile data need somebody to keep telling everyone where they moved to. A control plane is what buys you that. The threshold in practice is somewhere around five devices, or the first laptop that changes networks daily, or the first time you want a rule like "the contractor's laptop reaches the staging server and nothing else". Below that, use WireGuard and enjoy the quiet.

Does the Tailscale app on my phone work with Headscale?

Yes — it is the same official client, and pointing it at a different control server is a supported operation rather than a hack. The mechanics differ by platform and that is where people lose an evening. On Linux, macOS and Windows it is a command-line flag on the client (--login-server) or the equivalent setting. On Android the option is exposed in the app itself, behind the version number in the settings screen. On iOS, iPadOS and tvOS there is no in-app field: you install a small configuration profile that sets the control server URL before you sign in, which is fiddly the first time and permanent afterwards. The Headscale documentation keeps a per-platform page for exactly this and it is worth opening rather than guessing, because the Apple flow in particular has changed more than once. One caveat that is not platform-specific: an app that has already been signed in to Tailscale's own service usually needs to be logged out — and sometimes reinstalled — before it will accept a different server.

Do I need to run my own DERP relay?

Not to make the network work — but it is the difference between mostly private and consistently private, so decide on purpose. DERP servers are relays used when two devices cannot open a direct connection: symmetric NAT on both ends, a corporate firewall that eats UDP, a mobile carrier doing something creative. The relayed traffic is still end-to-end encrypted WireGuard, so a relay operator sees ciphertext, not content. What they see is metadata: which two devices are talking, how much and when. Out of the box, Headscale hands your devices Tailscale's public DERP map, which means those relays are Tailscale's — you have self-hosted the control plane and left the fallback data path with the company you were moving away from. Headscale ships an embedded DERP server; turning it on gives you a relay in the same Nordic datacentre as the control server. The honest trade-off is coverage: Tailscale runs relays on several continents and you run one, so a device on the wrong side of the world is better served by theirs. Running both, with yours in the map, is a perfectly reasonable middle.

Can I make the whole tailnet use my own DNS resolver?

Yes, and it is one of the better reasons to build this. Headscale can push a DNS configuration to every device that joins, so your laptop on café Wi-Fi and your phone on mobile data both resolve through a resolver you run — with filtering, with a cache, and without the local network seeing the queries. Point the tailnet's global nameserver at the tailnet IP of the box running your own Unbound or AdGuard Home and set the override so it wins against whatever DHCP handed the device. Two things to get right. The resolver must be reachable on the tailnet, not on the public internet — that is the whole point, and it also means you can finally close port 53 to the world. And MagicDNS, which gives every device a short name inside a suffix you choose, needs that suffix to be one you control and one that is not a parent or child of the control server's own hostname; getting that wrong is a confusing failure that looks like a DNS bug and is a configuration bug.

How big does the VPS need to be?

For the control plane alone: small, and then smaller than that. Headscale is a single Go binary holding a small database of devices, keys and policy; the coordination traffic is a trickle of long-poll connections, one per device, that carry almost nothing most of the time. A Sentinel — 2 vCPU, 4 GB, 120 GB NVMe, $3.90 a month — will run a household, a small company, or a few hundred devices without noticing. The sizing question only becomes interesting when the same box takes on a second job. An embedded DERP relay forwards real traffic, so it wants uplink rather than cores. An exit node forwards all of a device's traffic, so it wants uplink, a generous bandwidth allowance and a CPU that can keep up with encryption at line rate. If you plan on both, size for the traffic and treat the control server as the rounding error it is — or better, put the exit node on a second box and keep the control plane boring.

Is an exit node on my own VPS the same as using a commercial VPN?

Technically similar, and in privacy terms almost the opposite, so it is worth being clear-eyed. An exit node routes a device's entire internet connection through your VPS, so websites see the VPS address instead of your home or hotel address, and the local network sees one encrypted tunnel instead of your browsing. That much matches a commercial VPN. The difference is the crowd. A commercial VPN's address is shared by thousands of unrelated customers, which is precisely what makes it hard to attribute traffic to you; your exit node's address is used by you, stably, forever. Against a snooping café network or an ISP building a profile, the exit node is excellent. Against an observer correlating activity across sites over months, a dedicated address is a durable identifier and a shared one is not. Use the exit node for what it is good at — untrusted local networks, reaching your own services, a stable address for things that expect one — and read the pillar guide for the part of the threat model it does not cover.

What exactly do I have to back up?

Four things, and only one of them is obvious. The database — SQLite by default, PostgreSQL if you moved it — holds users, devices, public keys, routes and pre-authentication keys. The private key material generated on first boot is the part people forget: lose it and every device rejects the server it has been talking to for a year, which means re-enrolling the whole fleet by hand. The configuration file matters because it encodes the three names from step 02, and reconstructing them from memory is how people accidentally change one. And the policy file, if you keep the access rules on disk, is the thing you least want to rewrite. All four live in two directories and together weigh less than a photograph. Encrypt them, send them to a machine that is not this one — ideally in another jurisdiction, which is what the backups guide is for — and restore them onto a scratch VPS once, on purpose, so you know the file you have is the file you need.

Does NordBastion mind if I run a control server and an exit node on a VPS?

A control server is an ordinary, well-behaved workload — a little HTTPS, a little UDP, essentially no traffic — and there is nothing in the acceptable-use policy that touches it. An exit node deserves one sentence of thought rather than a policy lecture: everything a device sends through it leaves the internet from your VPS address, under your account, so whatever your devices do is what the address will be known for. That is fine and it is exactly what the feature is for; it is also why an exit node shared with people you do not know is a bad idea, in the same way that an open relay is. Bandwidth is unlimited on every tier and relayed traffic is normal traffic, so a DERP server and an exit node are both included rather than metered. Beyond that: the account does not need your name, the invoice settles in Monero or Bitcoin, and the machine holding the map of every device you own is not indexed by anybody.

Get the box

A control plane of your own, on an account that is not your name. KYC-free, crypto-paid.

Sentinel — 2 vCPU and 4 GB for $3.90 a month, in Helsinki, Stockholm, Oslo or Reykjavík. Enough for Headscale, Caddy and your own DERP relay, with room left for the exit node if you want it on the same box. No email at signup, no identity document, and no company database holding the list of every device you own.

Last reviewed · 2026-10-12 · Sources · Headscale documentation (configuration reference, client setup, ACLs, DERP, remote CLI), Tailscale client and exit-node documentation, WireGuard protocol paper, Caddy documentation · Cadence · yearly

Private hosting, built in the North.