Self-hosting a DERP relay for Tailscale and Headscale
When you use Roamtty to reach a server at home or at work, Tailscale first tries to connect your phone and the server directly. If that fails, traffic goes through a DERP relay. Tailscale's relays are spread across the world, but if none is near you (mainland China is the usual example), every keystroke can take 200–500 ms to echo. A DERP relay of your own, close to you, brings that down to tens of milliseconds.
This guide sets up the official derper and then adds it to Tailscale's hosted service or to a self-hosted Headscale. Commands were checked against the latest releases at the time of writing: Tailscale 1.104 and Headscale 0.29.5.
What DERP does
- Tailscale devices prefer a direct UDP connection (NAT traversal). When both ends sit behind carrier-grade NAT, symmetric NAT or a strict firewall, traversal fails. This is common on mobile networks.
- Traffic then goes through a DERP relay over HTTPS. The relay only forwards WireGuard-encrypted packets: it can't read them or impersonate your devices.
- The same
derperprocess also runs STUN (UDP 3478), which lets devices discover their public address. A nearby STUN server also helps direct connections succeed.
To see whether you are relayed, run tailscale status on the server. relay "tok" on the peer's line means traffic goes through Tokyo; direct 198.51.100.7:41641 means a direct connection.
Pick a setup
| You use | Steps |
|---|---|
| Tailscale's hosted service | Deploy derper, then add a derpMap to your policy file |
| Headscale, relay on the same VPS | Turn on Headscale's embedded DERP; no separate derper needed |
| Headscale, relay on another server | Deploy derper, then add it to derp.yaml |
Before you start
- A server with a public IP, close (in network terms) to where you usually are. One CPU and 1 GB of memory are plenty; SSH and mosh use very little bandwidth. Don't put it behind a load balancer, reverse proxy or CDN.
- A domain name with an A record pointing at the server. This guide uses
derp.example.comand the public IP203.0.113.10. If your DNS is on Cloudflare, set the record to "DNS only" (grey cloud): DERP does not work through the CDN proxy. No domain? See using an IP address. - Open ports: TCP 443 (DERP), TCP 80 (certificates and captive-portal checks), UDP 3478 (STUN), and allow ICMP. Open them in both your cloud provider's security group and the server firewall.
# Ubuntu / Debian with ufw
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
Servers in mainland China: providers block ports 80 and 443 for domains without an ICP filing. Read servers in mainland China first and use a high port instead.
Deploy derper
Both Tailscale's hosted service and a standalone Headscale relay use this section. The examples are for Ubuntu / Debian.
1. Build and install
derper has no prebuilt packages, so build it with Go. On ARM servers replace amd64 with arm64.
curl -fsSL https://go.dev/dl/go1.27.2.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
export PATH=$PATH:/usr/local/go/bin
go install tailscale.com/cmd/derper@latest
sudo install -m 755 ~/go/bin/derper /usr/local/bin/derper
New Tailscale clients occasionally need a newer derper. Every few months, run the last two commands again and restart the service.
2. Run it with systemd
sudo tee /etc/systemd/system/derper.service > /dev/null <<'EOF'
[Unit]
Description=Tailscale DERP server
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/derper -hostname derp.example.com -a :443 -http-port 80 -stun-port 3478 -certdir /var/lib/derper/certs
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now derper
By default derper requests a Let's Encrypt certificate on the first HTTPS request and renews it automatically, so the DNS record must already resolve and ports 80 and 443 must be reachable from the internet. Check it:
curl https://derp.example.com
If the HTML contains This is a Tailscale DERP server, it works. Otherwise read the log with journalctl -u derper -f.
3. Allow only your own devices
Out of the box, anyone who knows your domain can add it to their DERP map and use your bandwidth. Append one of these to the ExecStart line:
- Tailscale: install Tailscale on this server and sign in to the same tailnet (
curl -fsSL https://tailscale.com/install.sh | sh, thensudo tailscale up), and append-verify-clients. derper asks the local tailscaled whether each client belongs to your tailnet. Disable key expiry for this machine in the admin console, or every relay request is refused once its key expires. - Headscale: append
-verify-client-url=https://hs.example.com/verify -verify-client-url-fail-open=false. derper asks Headscale about each client and admits only devices Headscale knows. The second flag refuses connections while Headscale is unreachable; the default is to let them through.
sudo systemctl daemon-reload && sudo systemctl restart derper
Add it to Tailscale
In the Tailscale admin console, open Access controls, switch to the JSON editor and add a top-level derpMap to the policy file:
{
// keep your existing grants, acls, tagOwners and so on
"derpMap": {
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "myderp",
"RegionName": "My DERP",
"Nodes": [
{
"Name": "900a",
"RegionID": 900,
"HostName": "derp.example.com",
"IPv4": "203.0.113.10"
}
]
}
}
}
}
- Region IDs 900–999 are reserved for your own relays. Use one server per region; for more servers, add regions 901, 902 and so on.
IPv4is optional but recommended: devices can still reach the relay when DNS has problems.- If DERP is not on port 443, add
"DERPPort": 33445to the node; if STUN is not on 3478, add"STUNPort". - Tailscale's own regions stay as a fallback and devices pick the fastest region automatically. To use only your relay, add
"OmitDefaultRegions": truetoderpMap, but devices that can't connect directly will lose connectivity whenever that server is down.
Once saved, the new DERP map reaches all devices right away; no client restart is needed.
Headscale: the embedded DERP
Headscale has a DERP server built in. It uses the domain, port and certificate of server_url, so no separate derper is needed. In /etc/headscale/config.yaml:
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
verify_clients: true # only this Headscale's devices may use it
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
ipv4: 203.0.113.10 # the VPS's public IP
urls:
- https://controlplane.tailscale.com/derpmap/default # keep Tailscale's relays as a fallback
paths: []
auto_update_enabled: true
update_frequency: 3h
sudo ufw allow 3478/udp
sudo systemctl restart headscale
server_urlmust use https. Caddy as a reverse proxy works as is; with nginx, pass theUpgradeandConnectionheaders through.- If the VPS can't reach
controlplane.tailscale.com, Headscale fails to start because it can't download Tailscale's DERP map. Seturlsto[]to use only your own relay. - The embedded DERP isn't tuned for throughput. It's fine for personal use; switch to a standalone derper if you relay a lot of traffic.
Headscale: a standalone derper
To run the relay on another server closer to you, deploy it as in deploy derper (with -verify-client-url), then create /etc/headscale/derp.yaml on the Headscale VPS. All keys are lowercase:
regions:
900:
regionid: 900
regioncode: myderp
regionname: My DERP
nodes:
- name: 900a
regionid: 900
hostname: derp.example.com
ipv4: 203.0.113.10
stunport: 3478
derpport: 443
Reference it from config.yaml:
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
paths:
- /etc/headscale/derp.yaml
auto_update_enabled: true
update_frequency: 3h
Apply it with sudo systemctl restart headscale. You can also use the embedded DERP and a standalone derper together: keep server.enabled: true and list derp.yaml under paths.
Check that it works
On any computer or server in your network, run:
tailscale netcheck
Your relay should appear in the DERP latency list, with the lowest latency. The output looks like this:
* Nearest DERP: My DERP
* DERP latency:
- myderp: 12.3ms (My DERP)
- hkg: 48.9ms (Hong Kong)
- tok: 71.4ms (Tokyo)
The embedded Headscale DERP shows up with the region code headscale. You can also test connectivity and see the path to a peer:
tailscale debug derp myderp # TLS and STUN checks for that region
tailscale ping 100.64.0.2 # the peer's Tailscale IP
tailscale ping first reports via DERP(myderp), then via 198.51.100.7:41641 once a direct path is found. Nothing changes in Roamtty: keep connecting to the server's 100.x.y.z address.
Servers in mainland China
On cloud servers in mainland China, ports 80 and 443 are blocked for domains without an ICP filing, and Let's Encrypt validation fails with them. Use a high port (33445 below) and get the certificate through DNS validation instead.
1. Get a certificate with DNS validation
With DNS on Cloudflare and acme.sh (the API token needs DNS edit permission for the zone; acme.sh documents other DNS providers), run as root:
curl https://get.acme.sh | sh -s email=you@example.com
export CF_Token=<Cloudflare API token>
~/.acme.sh/acme.sh --issue --server letsencrypt --dns dns_cf -d derp.example.com
mkdir -p /etc/derper/certs
~/.acme.sh/acme.sh --install-cert -d derp.example.com \
--key-file /etc/derper/certs/derp.example.com.key \
--fullchain-file /etc/derper/certs/derp.example.com.crt \
--reloadcmd "systemctl restart derper"
The files must be named <domain>.crt and <domain>.key. acme.sh renews the certificate and restarts derper so it loads the new one.
2. Move to a high port
Change ExecStart in derper.service to the line below, which turns off port 80 and reads the certificate from the directory. Open TCP 33445 and UDP 3478 in the security group:
ExecStart=/usr/local/bin/derper -hostname derp.example.com -a :33445 -http-port -1 -stun-port 3478 -certmode manual -certdir /etc/derper/certs
Update the port in your DERP map too: "DERPPort": 33445 on the node in the Tailscale policy file, or derpport: 33445 in Headscale's derp.yaml.
No domain: using an IP address
With a public IP as -hostname and -certmode manual, derper generates a self-signed certificate and logs a node entry containing a CertName:
ExecStart=/usr/local/bin/derper -hostname 203.0.113.10 -a :33445 -http-port -1 -stun-port 3478 -certmode manual -certdir /etc/derper/certs
sudo journalctl -u derper | grep CertName
Add the logged "CertName": "sha256-raw:…" to your node (certname in derp.yaml) and set HostName to the IP. Clients then check the certificate's fingerprint, so you need neither a domain nor a CA-issued certificate, but every device must run Tailscale 1.82 or later.
Troubleshooting
| Symptom | What to do |
|---|---|
Your relay is missing from netcheck | Tailscale: make sure the policy file was saved. Headscale: make sure you restarted it, that tailscale debug derp-map lists the region, and that derp.yaml keys are lowercase |
| Your relay is listed without a latency | The DERP port is closed or the certificate is invalid: run curl -v https://derp.example.com from another machine (add :33445 for a custom port) and check the security group |
| Let's Encrypt fails | The domain doesn't resolve to this server, ports 80 / 443 are closed, or the Cloudflare proxy (orange cloud) is on. For servers in mainland China, see the previous section |
Nothing connects after adding -verify-clients | Tailscale on this server is signed out, expired or in another tailnet; check with tailscale status |
| Headscale won't start and the log mentions DERPMap | The VPS can't reach controlplane.tailscale.com; set urls to [] |
| Still slow through your own relay | Relay latency is the round trip from phone to relay to server, so pick a location close to both. Allowing UDP 41641 into the server helps direct connections |
For the basic Tailscale and Headscale setup, see connecting from anywhere in the documentation.