Getting Headscale up and running is one thing; keeping it working in production is a completely different story. This guide is about learning how to fix Headscale errors that show up after the initial setup, including:
- Nodes that refuse to register.
- Pre-auth keys that have gone stale.
- DERP fallback that never goes away.
- DNS names that do not resolve.
- Subnet routes that do not appear.
- ACL rules that block everything silently.
If you have not installed Headscale yet, start with this Headscale installation guide first, then come back here when things break to fix Headscale errors.
How to Read Headscale Logs
Before going into fixes, you need to understand what the server is actually telling you. One of the fastest ways to fix Headscale errors is to simply read the logs before changing any config.
Most people skip this step and spend hours guessing; don't do it.
Check the Headscale service logs
Use the commands below to check the service logs:
sudo journalctl -u headscale -f sudo journalctl -u headscale -n 100 --no-pager docker logs -f headscale
Enable debug logging
When the regular logs do not give you enough detail, switch to debug mode in your config.yaml:
After saving, restart the service:
sudo systemctl restart headscale
Check client-side logs and status
On the Tailscale client side, these commands will help you so much:
tailscale netcheck tailscale status --json tailscale dns status --all tailscale debug daemon-logs tailscale debug netmap
Run these checks before you do anything else. The answer is usually hiding in one of them.
Headscale Node Registration Problems
Node registration problems are the most common issue after a fresh install. The node connects to the server, but never shows up in headscale nodes list. Knowing how to fix Headscale errors with node registration saves a lot of frustration.
Problem 1: Expired or invalid pre-auth key
This is the most common cause of registration failures. The symptom is usually invalid preauth key or auth key expired in the server logs.
Check your current keys:
headscale preauthkeys list --user <USERNAME> docker exec -it headscale headscale preauthkeys list --user <USERNAME>
headscale preauthkeys create --user myuser --reusable --expiration 168h
Use --reusable if you need to register multiple devices with the same key. For production environments, keep expiration under 72 hours and rotate keys regularly.
Problem 2: Wrong server URL on the client
If the client cannot even reach the registration page, check your server URL in config.yaml and make sure the client uses the same URL:
curl -v https://headscale.example.com/health tailscale up --login-server https://headscale.example.com
The URL must include https:// and the correct port. Missing https:// or a wrong port causes a silent timeout.
Problem 3: Firewall blocking registration
If the server logs show zero incoming requests when you try to register a node, the firewall is blocking the traffic. Open these ports:
| Port |
Protocol |
Purpose |
| 443 |
TCP |
HTTPS and main Headscale endpoint |
| 80 |
TCP |
HTTP redirect and ACME cert renewal |
| 3478 |
UDP |
STUN for NAT traversal |
| 41641 |
UDP |
WireGuard direct connections |
sudo ufw allow 443/tcpsudo ufw allow 3478/udpsudo ufw allow 41641/udp
Problem 4: Clock sync issues
JWT-based auth tokens are time-sensitive. If your VPS clock differs by even a few minutes from the client's, registration will fail with token not yet valid even though the key looks fine:
timedatectl tailscale debug timesync sudo apt install -y ntpsudo systemctl enable --now ntp
Problem 5: Key prefix mismatch (older versions)
In older Headscale builds, the node key needed a nodekey: prefix during manual registration. If you are on an older version and manual CLI registration fails, check your key format. The fix is to upgrade to a stable recent release.
After you fix the issue, confirm the node actually registered:
You should see the node with a Tailscale IP and a status of online.
Fix Headscale Errors with DERP Relay Issues
DERP stands for Designated Encrypted Relay for Packets. It is used when two nodes cannot establish a direct peer-to-peer connection. Staying on DERP permanently is not broken, but it is slower than direct connections. Many people try to fix Headscale errors here without realizing the root cause is often a NAT or firewall problem.
Enable the built-in DERP server
The embedded DERP server is disabled by default. If you want to use your own DERP relay instead of Tailscale's public servers, enable it in config.yaml:
1derp:2 server:3 enabled: true4 region_id: 9995 region_code: "headscale"6 region_name: "Headscale Embedded DERP"7 ipv4: YOUR_SERVER_PUBLIC_IP8 ipv6: "" 9 stun_listen_addr: "0.0.0.0:3478"10 urls: []
Set urls: [] to stop using Tailscale's public DERP servers. Note that this makes your own DERP a single point of failure, so only do this if you know the server will be reliable.
Test DERP connectivity
View the current DERP map that the client sees and test the connection to the embedded DERP with the commands below:
tailscale debug derp-maptailscale debug derp headscale
If the DERP map shows your server but the test fails, check the following:
- Port
443/tcp must be open (DERP uses TLS)
- Port
3478/udp must be open (STUN)
- server_url in
config.yaml must use https://
Traffic always goes through DERP (no direct connection)
This is a very common complaint, especially on VPS servers. If your VPS is behind ISP-level NAT, meaning the server has an internal private IP, but the ISP maps a public IP to it, direct peer-to-peer connections may fail. The nodes fall back to DERP relay permanently.
Check with:
If the output says pong from … via DERP instead of via … (direct), you are relaying.
To fix it:
1. Set the correct public IPv4 in the DERP config. Headscale needs to know the real external IP:
1derp:2 server:3 ipv4: 203.0.113.1
2. Make sure port 41641/udp is open for WireGuard; direct connections use this port.
3. If the VPS is behind double-NAT with no control over the outer NAT, direct P2P is often impossible, and DERP relay is the expected behavior.
Timezone causing DERP failure
An incorrect system timezone on the server can break DERP authentication. If your DERP was working and suddenly stopped after a server restart, check the timezone:
timedatectlsudo timedatectl set-timezone UTC
DNS and MagicDNS Not Working in Headscale
DNS problems in Headscale usually appear as nodes that can ping each other by IP but not by hostname. When you need to fix Headscale errors related to DNS, the issue is always either MagicDNS being disabled in the config or the client not accepting DNS from the server.
Enable MagicDNS in config.yaml
If DNS resolution is not working at all, check that MagicDNS is properly enabled:
1dns:2 magic_dns: true3 base_domain: yourdomain.example.com4 nameservers:5 - 1.1.1.16 - 8.8.8.8
Restart Headscale after any DNS config change:
sudo systemctl restart headscale
Client is not using the tailnet DNS
The client must be told to accept DNS settings from the control server:
tailscale up --login-server https://headscale.example.com --accept-dns=true tailscale set --accept-dns=true
If you set --accept-dns=false earlier for any reason, DNS resolution through the tailnet will not work.
MagicDNS overriding local DNS on macOS
This is a known problem. When MagicDNS is active on macOS, it can hijack all DNS queries, including local network names. If local resolution breaks after joining the tailnet:
tailscale set --accept-dns=false
On macOS, you can also go to Preferences > Uncheck "Use Tailscale DNS settings".
DNS not resolving for split DNS (restricted nameservers)
If you have restricted nameservers configured but --accept-routes=false on the client, those restricted nameservers will not be used. Either enable route acceptance or move the DNS nameservers to the main nameservers list.
Verify DNS is working
Check what DNS settings the client is using:
tailscale dns status --all
Test resolution of a tailnet hostname:
dig nodeA.yourdomain.example.com
Test an extra_records entry:
dig grafana.yourdomain.example.com
If the hostname resolves correctly with dig but not in the browser, the application may be bypassing the system DNS.
Headscale Subnet Routes Not Appearing
Subnet routes let a Tailscale node act as a router for a local network, making devices without Tailscale reachable from the tailnet. If you need to fix Headscale errors with routing, the most common issue is forgetting to approve the route on the server side after advertising it.
Step 1: Advertise the route from the node
On the device that will act as the subnet router:
sudo tailscale up --login-server https://headscale.example.com --advertise-routes=192.168.1.0/24 sudo tailscale set --advertise-routes=192.168.1.0/24
Step 2: Check if the route appeared on the server
headscale nodes list-routes
You will see output like this:
ID | Hostname | Approved | Available | Serving (Primary)1 | myrouter | | 192.168.1.0/24 |
The Approved column is empty; this is the problem. Routes must be manually approved unless autoApprovers are configured.
Step 3: Approve the route on Headscale
headscale nodes approve-routes --identifier 1 --routes 192.168.1.0/24
ID | Hostname | Approved | Available | Serving (Primary)1 | myrouter | 192.168.1.0/24 | 192.168.1.0/24 | 192.168.1.0/24
Step 4: Enable route acceptance on the client
Clients will not use advertised routes unless they are told to accept them:
sudo tailscale set --accept-routes
Step 5: Enable IP forwarding on the router node
Without this, the subnet router will receive traffic but drop it instead of forwarding it:
sudo sysctl -w net.ipv4.ip_forward=1 echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-tailscale.confsudo sysctl -p /etc/sysctl.d/99-tailscale.conf
Route showing as "Pending approval" or not serving
This is a known bug in some Headscale versions where an already-approved route stops being propagated to the client. If the route shows Advertised: true but Enabled: false on the server:
sudo tailscale set --advertise-routes=""sudo tailscale set --advertise-routes=192.168.1.0/24
In some cases, you may need to restart tailscaled on the client as well.
Use autoApprovers to skip manual approval
For production setups, autoApprovers save you from manually approving every route. Add this to your policy file:
1{2 "tagOwners": {3 "tag:router": [4 "alice@"5 ]6 },7 "autoApprovers": {8 "routes": {9 "192.168.1.0/24": [10 "tag:router"11 ]12 }13 }14}
Then register the node with the correct tag:
sudo tailscale up --login-server https://headscale.example.com \ --advertise-tags tag:router \ --advertise-routes 192.168.1.0/24
ACL Policy Mistakes in Headscale
ACL mistakes are tricky because Headscale will often start up fine but silently block all traffic. When you need to fix Headscale errors caused by policy files, the first thing to check is whether the acls array is present but empty, which blocks everything by default.
The silent default: no ACL file = allow all
If you have not configured any ACL policy, all nodes can talk to each other freely. The moment you add a policy file with an acls array, anything not explicitly allowed is blocked. This surprises a lot of people.
A safe starting point is an explicit allow-all rule:
1{2 "acls": [3 {4 "action": "accept",5 "src": [6 "*"7 ],8 "dst": [9 "*:*"10 ]11 }12 ]13}
Point Headscale to your policy file
In config.yaml, the correct way to set the policy path depends on your version:
12policy:3 mode: file4 path: "/etc/headscale/acls.json"
If the newer format does not work after a version upgrade, try the legacy key:
acl_policy_path: "/etc/headscale/acls.json"
Reload after any ACL change:
sudo systemctl reload headscalesudo kill -HUP $(pidof headscale)
Find syntax errors in the ACL file
If Headscale fails to start or routes are not being applied, run this to get a detailed error:
sudo /usr/bin/headscale serve /etc/headscale/config.yaml
Look for the line number in the output. ACL files use HuJSON format, which allows comments (//) but is strict about commas and brackets.
Test after applying ACL rules
After applying a rule, test from both directions:
tailscale ping <destination-tailscale-ip> tailscale debug netmap
If tailscale ping works but an application connection does not, the problem is in the port rules inside the ACL, make sure the destination port is included in "dst".
ACL not working after Headscale upgrade
Some version upgrades change the expected policy format. If your old ACL file stopped working after an upgrade, check the Headscale release notes and compare your file structure against the new example files.
Need a more reliable control plane for remote access? Deploy Headscale on a PerLod Linux VPS and get a dedicated server with consistent uptime for your tailnet control plane.
Conclusion
Most Headscale issues come from a few common problems, including login errors, network blocks, DERP setup mistakes, DNS issues, unapproved routes, or ACL file errors. The best way to fix them is to check the server logs, check the client status, and see if the problem is on the VPS or the device.
Keep your keys fresh, your ports open, and a backup of your ACL file before changes.
We hope you enjoy this guide to fix Headscale errors. For advanced network setups where devices are behind CGNAT, you can check out the MikroTik CGNAT remote access guide for complementary routing strategies.
For more detailed information to fix Headscale errors, you can check the Tailscale Official Guide.