Headscale Troubleshooting Guide: Fix DERP, DNS, Route Advertising, and Node Registration Problems
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.
Table of Contents
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:
# Systemd-managed installs
sudo journalctl -u headscale -f
# View last 100 lines
sudo journalctl -u headscale -n 100 --no-pager
# Docker installs
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:
log:
level: debug
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:
# Check local network conditions
tailscale netcheck
# Full client status in JSON
tailscale status --json
# Check DNS resolution status
tailscale dns status --all
# View client daemon logs
tailscale debug daemon-logs
# Inspect current netmap
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:
# Native install
headscale preauthkeys list --user <USERNAME>
# Docker install
docker exec -it headscale headscale preauthkeys list --user <USERNAME>
Create a fresh key:
# Reusable key valid for 7 days
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:
# Test if the server endpoint is reachable
curl -v https://headscale.example.com/health
# Connect the client to the correct URL
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 |
# UFW example
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
sudo 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:
# Check server time
timedatectl
# Check client time sync
tailscale debug timesync
# Force NTP sync
sudo apt install -y ntp
sudo 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:
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:
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:
tailscale debug derp-map
tailscale debug derp headscale
If the DERP map shows your server but the test fails, check the following:
- Port
443/tcpmust be open (DERP uses TLS) - Port
3478/udpmust be open (STUN) - server_url in
config.yamlmust usehttps://
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:
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:
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:
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:
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:
sudo systemctl restart headscale
Client is not using the tailnet DNS
The client must be told to accept DNS settings from the control server:
# Make sure the client accepts DNS
tailscale up --login-server https://headscale.example.com --accept-dns=true
# Or update an existing connection
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:
# Disable DNS acceptance only on this node
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:
# New connection
sudo tailscale up --login-server https://headscale.example.com --advertise-routes=192.168.1.0/24
# Already registered node
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
After approval:
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:
# Temporary
sudo sysctl -w net.ipv4.ip_forward=1
# Permanent
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo 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:
# Re-advertise the route to force a refresh
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:
{
"tagOwners": {
"tag:router": ["alice@"]
},
"autoApprovers": {
"routes": {
"192.168.1.0/24": ["tag:router"]
}
}
}
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:
{
"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:
# Newer versions
policy:
mode: file
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 headscale
# or send SIGHUP
sudo 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:
# From the source node
tailscale ping <destination-tailscale-ip>
# Check what the client sees in its netmap
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.
FAQs
How do I reload the ACL without restarting Headscale?
Run sudo systemctl reload headscale or sudo kill -HUP $(pidof headscale). Headscale will log whether the ACL loaded successfully.
Why does traffic always go through DERP instead of direct?
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.
Why is my node showing as offline even after registration?
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.