headscale: run our own DERP relay instead of Tailscale's

By default headscale fetches https://controlplane.tailscale.com/derpmap/default
at startup and treats failure as fatal, so it cannot boot when that URL is
unreachable. A self-hosted control plane that will not start without
Tailscale's infrastructure rather misses the point of self-hosting -- and
it crash-looped for exactly that reason while neptun had no DNS.

Enable the embedded DERP server on region 999 and drop the upstream map.
The relay rides Caddy on :443, which is why that vhost already sets
flush_interval -1; only STUN needs a port of its own.

Verified against headscale 0.28.0 before committing: it starts clean with
urls = [], registers "DERP region: {RegionID:999 ...}" pointing at
vpn.mgaction.town with DERPPort 443, and brings up STUN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 09:45:29 +02:00
co-authored by Claude Opus 4.8
parent 4fedc80bb4
commit 3671841eca
+28
View File
@@ -20,6 +20,34 @@
base_domain = "hosts.mgaction.town"; base_domain = "hosts.mgaction.town";
nameservers.global = [ "1.1.1.1" "9.9.9.9" ]; nameservers.global = [ "1.1.1.1" "9.9.9.9" ];
}; };
# Run our own DERP relay instead of pulling Tailscale's map.
#
# With the default (urls = [controlplane.tailscale.com/derpmap/default],
# auto_update_enabled = true) headscale fetches that map at startup and
# treats failure as FATAL — so a DNS blip or a Tailscale outage stops the
# control server from booting at all. A self-hosted control plane that
# can't start without Tailscale's infrastructure rather misses the point.
#
# The relay itself rides Caddy on :443 (hence the flush_interval -1 on
# that vhost); only STUN needs its own UDP port.
derp = {
urls = [ ];
auto_update_enabled = false;
server = {
enabled = true;
region_id = 999; # 900-999 is the custom range
region_code = "neptun";
region_name = "neptun";
stun_listen_addr = "0.0.0.0:3478";
automatically_add_embedded_derp_region = true;
};
};
}; };
}; };
# STUN for the embedded DERP server above. Also needs a matching inbound-UDP
# rule in netcup's edge firewall — it is stateless and defaults to denying
# inbound UDP outright, which silently kills every DNS/NTP reply too.
networking.firewall.allowedUDPPorts = [ 3478 ];
} }