Headscale Troubleshooting: DERP, DNS, Routes & Node Registration

Updated on Aug 24, 2026
Mila H
11 MINS READ
Table of Contents
how to fix Headscale errors

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:

Bash
# Systemd-managed installssudo journalctl -u headscale -f # View last 100 linessudo journalctl -u headscale -n 100 --no-pager # Docker installsdocker logs -f headscale

Enable debug logging

When the regular logs do not give you enough detail, switch to debug mode in your config.yaml:

YAML
log:  level: debug

After saving, restart the service:

Bash
sudo systemctl restart headscale

Check client-side logs and status

On the Tailscale client side, these commands will help you so much:

Bash
# Check local network conditionstailscale netcheck # Full client status in JSONtailscale status --json # Check DNS resolution statustailscale dns status --all # View client daemon logstailscale debug daemon-logs # Inspect current netmaptailscale 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:

Bash
# Native installheadscale preauthkeys list --user <USERNAME> # Docker installdocker exec -it headscale headscale preauthkeys list --user <USERNAME>

Create a fresh key:

Bash
# Reusable key valid for 7 daysheadscale 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:

Bash
# Test if the server endpoint is reachablecurl -v https://headscale.example.com/health # Connect the client to the correct URLtailscale 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
Bash
# UFW examplesudo 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:

Bash
# Check server timetimedatectl # Check client time synctailscale debug timesync # Force NTP syncsudo 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:

Bash
headscale nodes list

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:

YAML
derp:  server:    enabled: true    region_id: 999    region_code: "headscale"    region_name: "Headscale Embedded DERP"    ipv4: YOUR_SERVER_PUBLIC_IP    ipv6: ""  # optional    stun_listen_addr: "0.0.0.0:3478"  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:

Bash
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:

Bash
tailscale ping <NODE_IP>

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:

YAML
derp:  server:    ipv4: 203.0.113.1  # Your actual public IP

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:

Bash
timedatectl# If wrong, fix it:sudo 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:

YAML
dns:  magic_dns: true  base_domain: yourdomain.example.com  nameservers:    - 1.1.1.1    - 8.8.8.8

Restart Headscale after any DNS config change:

Bash
sudo systemctl restart headscale

Client is not using the tailnet DNS

The client must be told to accept DNS settings from the control server:

Bash
# Make sure the client accepts DNStailscale up --login-server https://headscale.example.com --accept-dns=true # Or update an existing connectiontailscale 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:

Bash
# Disable DNS acceptance only on this nodetailscale 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:

Bash
tailscale dns status --all

Test resolution of a tailnet hostname:

Bash
dig nodeA.yourdomain.example.com

Test an extra_records entry:

Bash
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:

Bash
# New connectionsudo tailscale up --login-server https://headscale.example.com --advertise-routes=192.168.1.0/24 # Already registered nodesudo tailscale set --advertise-routes=192.168.1.0/24

Step 2: Check if the route appeared on the server

Bash
headscale nodes list-routes

You will see output like this:

Bash
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

Bash
headscale nodes approve-routes --identifier 1 --routes 192.168.1.0/24

After approval:

Bash
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:

Bash
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:

Bash
# Temporarysudo sysctl -w net.ipv4.ip_forward=1 # Permanentecho "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:

Bash
# Re-advertise the route to force a refreshsudo 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:

JSON
{  "tagOwners": {    "tag:router": [      "alice@"    ]  },  "autoApprovers": {    "routes": {      "192.168.1.0/24": [        "tag:router"      ]    }  }}

Then register the node with the correct tag:

Bash
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:

JSON
{  "acls": [    {      "action": "accept",      "src": [        "*"      ],      "dst": [        "*:*"      ]    }  ]}

Point Headscale to your policy file

In config.yaml, the correct way to set the policy path depends on your version:

YAML
# Newer versionspolicy:  mode: file  path: "/etc/headscale/acls.json"

If the newer format does not work after a version upgrade, try the legacy key:

YAML
acl_policy_path: "/etc/headscale/acls.json"

Reload after any ACL change:

Bash
sudo systemctl reload headscale# or send SIGHUPsudo 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:

Bash
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:

Bash
# From the source nodetailscale ping <destination-tailscale-ip> # Check what the client sees in its netmaptailscale 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.

Run sudo systemctl reload headscale or sudo kill -HUP $(pidof headscale). Headscale will log whether the ACL loaded successfully.

Your VPS is probably behind ISP NAT, or the WireGuard port 41641/udp is blocked. Set the correct public IP in your DERP config and open the necessary ports.

It is usually a firewall issue. Make sure ports 443/tcp and 3478/udp are open on the VPS. Run tailscale netcheck on the client to see what is blocked.