Self-Host Tailscale DERP Relay Server
When using Tailscale's official DERP relay servers from within China, you often encounter high latency and unstable connections. Hosting your own DERP server on a VPS with a public IP can significantly improve communication latency between nodes and the success rate of hole punching. Below are two Docker-based deployment methods: the domain reverse proxy mode and the pure IP mode.
Prerequisites
- A VPS with a public IP (choose a region with low latency)
- Docker and Docker Compose installed
- A domain name with DNS configured (required for Method 1, not for Method 2)
- Nginx (or another reverse proxy) to handle SSL (required for Method 1, not for Method 2)
Method 1: Deploy DERP with a Domain (Reverse Proxy Mode)
Docker Compose Configuration
Create docker-compose.yml:
services:
derper:
container_name: derper
image: fredliang/derper
restart: always
network_mode: host # Use host networking directly, supports IPv6
environment:
- DERP_DOMAIN=derp.example.com
- DERP_ADDR=:13477
- DERP_HTTP_PORT=13478
- DERP_VERIFY_CLIENTS=true
volumes:
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
Key Configuration Notes
| Configuration Item | Description |
|---|---|
| network_mode: host | Required. Uses the host network stack, supports IPv6 and STUN |
| DERP_DOMAIN | Your DERP server domain name |
| DERP_ADDR | Listen address and port |
| DERPVERIFYCLIENTS | Set to true to verify client identity and prevent abuse |
| tailscaled.sock | Mount the Tailscale socket for client verification |
⚠️ Note:
DERP_VERIFY_CLIENTS=truerequires the Tailscale client to be running on the server and its socket file to be mounted.
Start the Service
docker compose up -d
Nginx Reverse Proxy Configuration
DERP uses WebSocket for communication, so the reverse proxy needs to be configured correctly.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name derp.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location ^~ / {
proxy_pass http://127.0.0.1:13477;
# Core: Force sending WebSocket headers
# Fixes 426 errors caused by missing headers under HTTP/2
proxy_set_header Upgrade "websocket";
proxy_set_header Connection "upgrade";
# Required: Protocol version and disable caching
proxy_http_version 1.1;
proxy_buffering off;
# Basic headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# Timeout (keep long connections)
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
}
}
Important Configuration Explanation
- Force WebSocket headers:
proxy_set_header Upgrade "websocket"andConnection "upgrade"are mandatory; otherwise, you'll get a 426 error under HTTP/2 - Use HTTP/1.1:
proxy_http_version 1.1ensures WebSocket works correctly - Disable buffering:
proxy_buffering offprevents relay data from being cached - Long timeout: 86400 seconds (24 hours) keeps the connection alive
Reload Nginx:
sudo nginx -t && sudo nginx -s reload
Tailscale ACL Configuration
Log in to the Tailscale Admin Console, go to Access Controls, and add the derpMap configuration:
{
// Custom DERP server
"derpMap": {
// Use only custom DERP, disable official DERP
"OmitDefaultRegions": true,
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "hk",
"RegionName": "DMIT Cloud Hong Kong",
"Nodes": [
{
"Name": "hk1",
"RegionID": 900,
"HostName": "derp.example.com",
"DERPPort": 443,
"STUNPort": 3478
}
]
}
}
},
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
]
}
Configuration Notes
| Configuration Item | Description |
|---|---|
| OmitDefaultRegions | Set to true to disable official DERP, use only self-hosted |
| RegionID | Custom region ID, recommended to use 900+ to avoid conflicts |
| RegionCode | Region code, e.g., hkg, sgp, tyo |
| HostName | DERP server domain name |
| DERPPort | DERP port, which becomes 443 after Nginx reverse proxy |
| STUNPort | STUN port, keep as 3478 |
Optional: Specify IP Address
If you don't want to use DNS resolution, you can specify the IP directly:
{
"Name": "hk1",
"RegionID": 900,
"HostName": "derp.example.com",
"IPv4": "[IP_ADDRESS]",
"IPv6": "[IP_ADDRESS]",
"DERPPort": 443,
"STUNPort": 3478
}
Method 2: Deploy DERP Without a Domain (Pure IP Mode)
If you don't have a domain, or don't want to deal with DNS and SSL certificates, you can deploy a DERP server directly using an IP address. This method uses the ip_derper image, which automatically generates self-signed certificates, eliminating the need for an Nginx reverse proxy.
Docker Compose Configuration
Create docker-compose.yml:
services:
derper:
container_name: derper
image: ghcr.io/yangchuansheng/ip_derper:latest
restart: always
network_mode: host # Use host networking directly, supports IPv6
environment:
- DERP_HOST=Your server IP
- DERP_ADDR=:13477
- DERP_HTTP_PORT=13478
- DERP_CERTS=/app/certs
- DERP_STUN=true
- DERP_VERIFY_CLIENTS=true
volumes:
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock:ro
- ./certs:/app/certs
Key Configuration Notes
| Configuration Item | Description |
|---|---|
| ip_derper image | Based on the official derper, modified to support pure IP operation, automatically generates self-signed certificates |
| DERP_ADDR | Listen port, here using 13477 |
| DERP_CERTS | Path for self-signed certificates (automatically generated inside the container) |
| DERPVERIFYCLIENTS | Set to true to verify client identity and prevent abuse |
| network_mode: host | Uses the host network stack, supports IPv6 and STUN |
💡 Difference from the domain version: The no-domain version does not require configuring an Nginx reverse proxy. The DERP server exposes the port directly and uses self-signed TLS certificates.
Start the Service
docker compose up -d
ACL Configuration
Log in to the Tailscale Admin Console, go to Access Controls, and add the derpMap configuration:
{
// Custom DERP server
"derpMap": {
// Use only custom DERP, disable official DERP; true means use only your own nodes, false means your own nodes + official nodes
"OmitDefaultRegions": false,
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "chn",
"RegionName": "China",
"Nodes": [
{
"Name": "server",
"RegionID": 900,
"IPv4": "[IP_ADDRESS]",
"IPv6": "[IP_ADDRESS]",
"InsecureForTests": true,
"DERPPort": 13477,
"STUNPort": 3478
}
]
}
}
}
}
Core Configuration Notes
| Configuration Item | Description |
|---|---|
| OmitDefaultRegions | false means use both self-hosted and official DERP; true means use only self-hosted |
| IPv4 / IPv6 | Directly specify the server IP address, no DNS resolution |
| InsecureForTests | Core configuration: set to true to skip TLS certificate verification (because self-signed certificates are used) |
| DERPPort | Must match the port configured in DERP_ADDR, here it is 13477 |
| STUNPort | STUN port, default is 3478; set to -1 to disable STUN for this node |
⚠️ Note: Although
InsecureForTestscontains "ForTests" in its name, it is an official Tailscale configuration option for skipping TLS verification and is necessary in pure IP, no-domain scenarios.
Server Firewall Configuration
Regardless of which deployment method you choose, you must allow the corresponding ports in the system firewall and cloud provider security groups (e.g., Alibaba Cloud, Tencent Cloud, etc.):
| Method | Port | Protocol | Purpose |
|---|---|---|---|
| Method 1 (Reverse Proxy) | 443 | TCP | Nginx receives HTTPS/DERP traffic |
| Method 2 (Pure IP) | 13477 | TCP | DERP Docker native relay listening |
| All Methods | 3478 | UDP | STUN protocol, used for NAT hole punching |
# UFW Example: Method 1 (reverse proxy deployment, allow HTTPS)
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
# UFW Example: Method 2 (pure IP deployment, allow directly)
sudo ufw allow 13477/tcp
sudo ufw allow 3478/udp
Verify the DERP Server
Check DERP Status
On any Tailscale client, run:
tailscale netcheck
The output should show your custom DERP region and its latency:
Report:
* UDP: true
* IPv4: yes, ...
* IPv6: yes, ...
* DERP latency:
- hkg: 25.3ms (Tencent Cloud Hong Kong)
Check Connection Method
tailscale status
If it shows relay "hkg" or similar, it means you are using your DERP relay.
Frequently Asked Questions
Q: Status shows IPv6: No
Check the following:
- Whether the server has an IPv6 address
- Whether DNS has AAAA records configured
- Whether Docker is using
network_mode: host - Whether the firewall allows IPv6
Q: 426 Upgrade Required Error
Nginx configuration issue, ensure:
proxy_set_header Upgrade "websocket"is setproxy_set_header Connection "upgrade"is setproxy_http_version 1.1is used
Q: Client Verification Fails
Ensure:
- The Tailscale client is running on the server
/var/run/tailscale/tailscaled.sockis correctly mounted- The container has permission to access the socket
Summary
The main benefits of self-hosting a Tailscale DERP server:
- Access private nodes and reduce latency for cross-region communication.
- Traffic does not go through official servers.
- Optionally deploy nodes in specific regions, such as cloud servers in China.
- Supports IPv6, increasing the probability of successful P2P hole punching.
Key configuration points:
- Docker's
network_modemust be set tohost. - In domain reverse proxy mode, Nginx must include WebSocket headers (
Upgrade "websocket", etc.) to avoid 426 errors. - In pure IP mode, the ACL configuration must include
"InsecureForTests": true. - The
OmitDefaultRegionssetting in the ACL controls whether to disable official DERP nodes.