Tailscale Self-Hosted DERP Relay Server
DERP (Designated Encrypted Relay for Packets) is the relay and connectivity component of Tailscale. It primarily handles two tasks:
- Assisting nodes with NAT traversal (hole-punching) to establish direct connections
- Relaying traffic when direct connections fail
Two deployment approaches:
- Docker: Quick to set up, relies on the container ecosystem, upgrade by swapping the image
- Non-Docker: System service + binary, closer to system-level operations, upgrade by replacing the binary
Quick Navigation
Choose the appropriate section based on your operating system and deployment method:
| OS | Docker Deployment | Non-Docker Deployment |
|---|---|---|
| Debian / Ubuntu | §3.1 | §4.1 |
| RHEL / CentOS / Rocky | §3.2 | §4.2 |
| Alpine | §3.3 | §4.3 |
After completing deployment, all paths converge to: §5 ACL Configuration → §6 Client Reconnection → §7 Testing & Verification
1. Deployment Parameters
This guide uses placeholders for values that need to be replaced. When you encounter these placeholders in subsequent steps, replace them with your actual values.
| Placeholder | Description | Example |
|---|---|---|
<VPS_IP> |
VPS public IP | xxx.xxx.xxx.xxx |
<DERP_TCP_PORT> |
DERP TCP port | 13477 |
<STUN_UDP_PORT> |
STUN UDP port | 13478 |
<REGION_ID> |
Custom DERP region ID | 900 |
<REGION_CODE> |
Custom DERP region code | tyxy |
<RELAY_HOSTNAME> |
DERP server’s Tailscale IP (first column of tailscale status) |
100.x.x.x |
If you want to do a quick run-through first, you can replace
<VPS_IP>,<DERP_TCP_PORT>,<STUN_UDP_PORT>withxxx.xxx.xxx.xxx,13477,13478.
2. Common Prerequisites
2.1 Check Server Resources and Network Info
free -m
df -h
ip addr show
2.2 Open Security Group and Firewall Ports
<DERP_TCP_PORT>/tcp: DERP service port<STUN_UDP_PORT>/udp: STUN service port
It is recommended to check both “cloud provider security groups + system firewall” rules to avoid situations where local access works but external access does not.
3. Docker Deployment
3.1 Debian / Ubuntu
Install Docker and Compose
apt update
apt install -y docker.io docker-compose-plugin
systemctl enable --now docker
docker --version
docker compose version
Optional: Configure Mirror Acceleration
When pulling images is slow on domestic networks, you can configure acceleration mirrors. This configuration persists after writing.
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://dockerproxy.com",
"https://docker.m.daocloud.io"
]
}
EOF
systemctl restart docker
For low-spec VPS, you can add the following parameters to
daemon.json:{ "registry-mirrors": ["..."], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "max-concurrent-downloads": 2 }
log-opts: Limits each log file to 10MB maximum with 3 files retained, preventing disk from being filled by logsmax-concurrent-downloads: Limits concurrent layer downloads to 2, reducing memory and bandwidth usage
Create docker-compose.yml
services:
derper:
image: ghcr.io/yangchuansheng/ip_derper
container_name: derper
restart: unless-stopped
ports:
- <DERP_TCP_PORT>:<DERP_TCP_PORT> # Replace with DERP port, e.g., 13477
- <STUN_UDP_PORT>:3478/udp # Replace with STUN port, e.g., 13478
environment:
- DERP_ADDR=:<DERP_TCP_PORT> # Replace with DERP port, format: :<port>
- DERP_VERIFY_CLIENTS=true # Enable client verification to prevent public abuse
volumes:
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
- The STUN internal container port is fixed at
3478/udp; the left side is the host-mapped port
Start and Verify
docker compose up -d
docker logs derper
ss -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>" # Replace with actual port numbers
Install Tailscale Client
curl -fsSL https://tailscale.com/install.sh | sh
systemctl enable --now tailscaled
tailscale up
Copy the https://login.tailscale.com/a/... authentication link from the terminal to your browser to complete login, then run tailscale status to confirm the status.
3.2 RHEL / CentOS / Rocky / AlmaLinux
Install Docker and Compose
dnf install -y docker docker-compose-plugin
systemctl enable --now docker
docker --version
docker compose version
Optional: Configure Mirror Acceleration
When pulling images is slow on domestic networks, you can configure acceleration mirrors. This configuration persists after writing.
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://dockerproxy.com",
"https://docker.m.daocloud.io"
]
}
EOF
systemctl restart docker
For low-spec VPS, you can add the following parameters to
daemon.json:{ "registry-mirrors": ["..."], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "max-concurrent-downloads": 2 }
log-opts: Limits each log file to 10MB maximum with 3 files retained, preventing disk from being filled by logsmax-concurrent-downloads: Limits concurrent layer downloads to 2, reducing memory and bandwidth usage
Create docker-compose.yml
services:
derper:
image: ghcr.io/yangchuansheng/ip_derper
container_name: derper
restart: unless-stopped
ports:
- <DERP_TCP_PORT>:<DERP_TCP_PORT> # Replace with DERP port, e.g., 13477
- <STUN_UDP_PORT>:3478/udp # Replace with STUN port, e.g., 13478
environment:
- DERP_ADDR=:<DERP_TCP_PORT> # Replace with DERP port, format: :<port>
- DERP_VERIFY_CLIENTS=true # Enable client verification to prevent public abuse
volumes:
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
- The STUN internal container port is fixed at
3478/udp; the left side is the host-mapped port
Start and Verify
docker compose up -d
docker logs derper
ss -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>" # Replace with actual port numbers
Install Tailscale Client
curl -fsSL https://tailscale.com/install.sh | sh
systemctl enable --now tailscaled
tailscale up
Copy the https://login.tailscale.com/a/... authentication link from the terminal to your browser to complete login, then run tailscale status to confirm the status.
3.3 Alpine
Install Docker and Compose
apk add docker docker-compose
rc-update add docker boot
service docker start
docker --version
docker-compose --version
Note: On Alpine, Compose is typically V1 (
docker-compose). For V2, you can install thedocker-compose-pluginbinary separately.
Optional: Configure Mirror Acceleration
When pulling images is slow on domestic networks, you can configure acceleration mirrors. This configuration persists after writing.
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://dockerproxy.com",
"https://docker.m.daocloud.io"
]
}
EOF
service docker restart
For low-spec VPS, you can add the following parameters to
daemon.json:{ "registry-mirrors": ["..."], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "max-concurrent-downloads": 2 }
log-opts: Limits each log file to 10MB maximum with 3 files retained, preventing disk from being filled by logsmax-concurrent-downloads: Limits concurrent layer downloads to 2, reducing memory and bandwidth usage
Create docker-compose.yml
services:
derper:
image: ghcr.io/yangchuansheng/ip_derper
container_name: derper
restart: unless-stopped
ports:
- <DERP_TCP_PORT>:<DERP_TCP_PORT> # Replace with DERP port, e.g., 13477
- <STUN_UDP_PORT>:3478/udp # Replace with STUN port, e.g., 13478
environment:
- DERP_ADDR=:<DERP_TCP_PORT> # Replace with DERP port, format: :<port>
- DERP_VERIFY_CLIENTS=true # Enable client verification to prevent public abuse
volumes:
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
- The STUN internal container port is fixed at
3478/udp; the left side is the host-mapped port
Start and Verify
docker-compose up -d
docker logs derper
netstat -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>" # Replace with actual port numbers
Install Tailscale Client
Alpine does not support the official one-line install script (
curl -fsSL https://tailscale.com/install.sh | sh); you must install viaapk.
Ensure /etc/apk/repositories contains the community repository (using Alpine 3.19 as an example):
http://dl-cdn.alpinelinux.org/alpine/v3.19/main
http://dl-cdn.alpinelinux.org/alpine/v3.19/community
If missing, append it:
echo "http://dl-cdn.alpinelinux.org/alpine/v3.19/community" >> /etc/apk/repositories
Install and start:
apk update
apk add tailscale
rc-update add tailscale default
rc-service tailscale start
tailscale up
Copy the https://login.tailscale.com/a/... authentication link from the terminal to your browser to complete login, then run tailscale status to confirm the status.
4. Non-Docker Deployment
The official Tailscale package does not include the
derperbinary; it needs to be compiled from source.
4.1 Debian / Ubuntu
System Preparation
apt update
apt upgrade -y
apt install -y wget openssl curl netcat-openbsd
Create Directory Structure
mkdir -p /usr/local/derp/{bin,certs}
Compile derper
Install Go environment:
apt install -y golang
go version
Switch download source for users in mainland China (Go 1.13+):
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
Compile (requires >= 2GB RAM):
go install tailscale.com/cmd/derper@latest
cp ~/go/bin/derper /usr/local/derp/bin/
Low-memory VPS may encounter OOM during compilation. Use §4.4 Cross-Compilation on Another Machine instead, then return to the “Verify Compilation Result” step in this section.
Verify Compilation Result
chmod +x /usr/local/derp/bin/derper
# Verify it's a static binary
ldd /usr/local/derp/bin/derper # Should output "not a dynamic executable"
# Verify version
/usr/local/derp/bin/derper --version
--versionoutput like1.xx.x-ERR-BuildInfois normal (go installbuilds don’t contain complete build info).
If you encounter Permission denied, /usr/local may be mounted with noexec:
# Check mount options
mount | grep /usr/local
# If noexec is present, copy to another location to run
cp /usr/local/derp/bin/derper /root/derper
chmod +x /root/derper
/root/derper --version
After confirming it works, move the binary back to /usr/local/derp/bin/ or adjust the mount options.
Generate Self-Signed Certificate
Key point: Certificate filenames must match the
-hostnameparameter. It’s recommended to use<VPS_IP>.crtand<VPS_IP>.keydirectly.
cd /usr/local/derp/certs
# Replace all <VPS_IP> below with your server's public IP
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -keyout <VPS_IP>.key -out <VPS_IP>.crt \
-subj "/CN=<VPS_IP>" \
-addext "subjectAltName=IP:<VPS_IP>"
-keyout <VPS_IP>.key: Private key output file, filename must match-hostname-out <VPS_IP>.crt: Certificate output file, filename must match-hostname-subj "/CN=<VPS_IP>": Certificate Common Name, set to server IP-addext "subjectAltName=IP:<VPS_IP>": SAN extension, must include the IP-days 3650: Valid for 10 years
Create systemd Service
cat > /etc/systemd/system/derper.service << 'EOF'
[Unit]
Description=Tailscale DERP Server
After=network.target
Wants=network.target
[Service]
User=root
Group=root
# Replace <VPS_IP>, <DERP_TCP_PORT>, <STUN_UDP_PORT> below with actual values (see §1)
ExecStart=/usr/local/derp/bin/derper \
-certmode=manual \
-certdir=/usr/local/derp/certs \
-hostname=<VPS_IP> \
-a :<DERP_TCP_PORT> \
-stun-port=<STUN_UDP_PORT> \
-http-port=-1 \
--verify-clients
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
Key parameters:
-certmode=manual: Manual certificate mode-http-port=-1: Disable HTTP port--verify-clients: Enable client verificationRestart=on-failure+RestartSec=5: Auto-restart on crash
Start and Verify
systemctl daemon-reload
systemctl enable derper
systemctl start derper
systemctl status derper
# Check listening ports (replace with actual port numbers)
ss -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>"
# Local TCP connectivity (replace with DERP port)
nc -zv 127.0.0.1 <DERP_TCP_PORT>
# View process
ps aux | grep derper
# Check logs if startup fails
journalctl -u derper --no-pager -n 30
Expected output:
tcp 0 0 :::<DERP_TCP_PORT> :::* LISTEN 1234/derper
udp 0 0 :::<STUN_UDP_PORT> :::* 1234/derper
If a line is missing, the corresponding port is not listening. Check the service logs.
Install Tailscale Client
curl -fsSL https://tailscale.com/install.sh | sh
systemctl enable --now tailscaled
tailscale up
Copy the https://login.tailscale.com/a/... authentication link from the terminal to your browser to complete login, then run tailscale status to confirm the status.
4.2 RHEL / CentOS / Rocky / AlmaLinux
System Preparation
dnf update -y
dnf install -y wget openssl curl nmap-ncat
Create Directory Structure
mkdir -p /usr/local/derp/{bin,certs}
Compile derper
Install Go environment:
dnf install -y golang
go version
Switch download source for users in mainland China (Go 1.13+):
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
Compile (requires >= 2GB RAM):
go install tailscale.com/cmd/derper@latest
cp ~/go/bin/derper /usr/local/derp/bin/
Low-memory VPS may encounter OOM during compilation. Use §4.4 Cross-Compilation on Another Machine instead, then return to the “Verify Compilation Result” step in this section.
Verify Compilation Result
chmod +x /usr/local/derp/bin/derper
# Verify it's a static binary
ldd /usr/local/derp/bin/derper # Should output "not a dynamic executable"
# Verify version
/usr/local/derp/bin/derper --version
--versionoutput like1.xx.x-ERR-BuildInfois normal (go installbuilds don’t contain complete build info).
If you encounter Permission denied, /usr/local may be mounted with noexec:
# Check mount options
mount | grep /usr/local
# If noexec is present, copy to another location to run
cp /usr/local/derp/bin/derper /root/derper
chmod +x /root/derper
/root/derper --version
After confirming it works, move the binary back to /usr/local/derp/bin/ or adjust the mount options.
Generate Self-Signed Certificate
Key point: Certificate filenames must match the
-hostnameparameter. It’s recommended to use<VPS_IP>.crtand<VPS_IP>.keydirectly.
cd /usr/local/derp/certs
# Replace all <VPS_IP> below with your server's public IP
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -keyout <VPS_IP>.key -out <VPS_IP>.crt \
-subj "/CN=<VPS_IP>" \
-addext "subjectAltName=IP:<VPS_IP>"
-keyout <VPS_IP>.key: Private key output file, filename must match-hostname-out <VPS_IP>.crt: Certificate output file, filename must match-hostname-subj "/CN=<VPS_IP>": Certificate Common Name, set to server IP-addext "subjectAltName=IP:<VPS_IP>": SAN extension, must include the IP-days 3650: Valid for 10 years
Create systemd Service
cat > /etc/systemd/system/derper.service << 'EOF'
[Unit]
Description=Tailscale DERP Server
After=network.target
Wants=network.target
[Service]
User=root
Group=root
# Replace <VPS_IP>, <DERP_TCP_PORT>, <STUN_UDP_PORT> below with actual values (see §1)
ExecStart=/usr/local/derp/bin/derper \
-certmode=manual \
-certdir=/usr/local/derp/certs \
-hostname=<VPS_IP> \
-a :<DERP_TCP_PORT> \
-stun-port=<STUN_UDP_PORT> \
-http-port=-1 \
--verify-clients
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
Key parameters:
-certmode=manual: Manual certificate mode-http-port=-1: Disable HTTP port--verify-clients: Enable client verificationRestart=on-failure+RestartSec=5: Auto-restart on crash
Start and Verify
systemctl daemon-reload
systemctl enable derper
systemctl start derper
systemctl status derper
# Check listening ports (replace with actual port numbers)
ss -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>"
# Local TCP connectivity (replace with DERP port)
nc -zv 127.0.0.1 <DERP_TCP_PORT>
# View process
ps aux | grep derper
# Check logs if startup fails
journalctl -u derper --no-pager -n 30
Expected output:
tcp 0 0 :::<DERP_TCP_PORT> :::* LISTEN 1234/derper
udp 0 0 :::<STUN_UDP_PORT> :::* 1234/derper
If a line is missing, the corresponding port is not listening. Check the service logs.
Install Tailscale Client
curl -fsSL https://tailscale.com/install.sh | sh
systemctl enable --now tailscaled
tailscale up
Copy the https://login.tailscale.com/a/... authentication link from the terminal to your browser to complete login, then run tailscale status to confirm the status.
4.3 Alpine
System Preparation
apk update
apk upgrade
apk add wget openssl curl netcat-openbsd
Create Directory Structure
mkdir -p /usr/local/derp/{bin,certs}
Compile derper
Install Go environment:
apk add go
go version
Switch download source for users in mainland China (Go 1.13+):
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
Compile (requires >= 2GB RAM):
go install tailscale.com/cmd/derper@latest
cp ~/go/bin/derper /usr/local/derp/bin/
Low-memory VPS may encounter OOM during compilation. Use §4.4 Cross-Compilation on Another Machine instead, then return to the “Verify Compilation Result” step in this section.
Verify Compilation Result
chmod +x /usr/local/derp/bin/derper
# Verify it's a static binary
ldd /usr/local/derp/bin/derper # Should output "not a dynamic executable"
# Verify version
/usr/local/derp/bin/derper --version
--versionoutput like1.xx.x-ERR-BuildInfois normal (go installbuilds don’t contain complete build info).
If you encounter Permission denied, /usr/local may be mounted with noexec:
# Check mount options
mount | grep /usr/local
# If noexec is present, copy to another location to run
cp /usr/local/derp/bin/derper /root/derper
chmod +x /root/derper
/root/derper --version
After confirming it works, move the binary back to /usr/local/derp/bin/ or adjust the mount options.
Generate Self-Signed Certificate
Key point: Certificate filenames must match the
-hostnameparameter. It’s recommended to use<VPS_IP>.crtand<VPS_IP>.keydirectly.
cd /usr/local/derp/certs
# Replace all <VPS_IP> below with your server's public IP
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -keyout <VPS_IP>.key -out <VPS_IP>.crt \
-subj "/CN=<VPS_IP>" \
-addext "subjectAltName=IP:<VPS_IP>"
-keyout <VPS_IP>.key: Private key output file, filename must match-hostname-out <VPS_IP>.crt: Certificate output file, filename must match-hostname-subj "/CN=<VPS_IP>": Certificate Common Name, set to server IP-addext "subjectAltName=IP:<VPS_IP>": SAN extension, must include the IP-days 3650: Valid for 10 years
Create OpenRC Service
cat > /etc/init.d/derper << 'EOF'
#!/sbin/openrc-run
description="Tailscale DERP Server"
command="/usr/local/derp/bin/derper"
# Replace <VPS_IP>, <DERP_TCP_PORT>, <STUN_UDP_PORT> below with actual values (see §1)
command_args="-certmode=manual -certdir=/usr/local/derp/certs -hostname=<VPS_IP> -a :<DERP_TCP_PORT> -stun-port=<STUN_UDP_PORT> -http-port=-1 --verify-clients"
command_user="root:root"
supervisor=supervise-daemon
respawn_delay=5
respawn_max=0
depend() {
need net
after firewall
}
EOF
Key parameters:
-certmode=manual: Manual certificate mode-http-port=-1: Disable HTTP port--verify-clients: Enable client verificationsupervisor=supervise-daemon+respawn_delay=5: Auto-restart on crash (equivalent to systemd’sRestart=on-failure)
Start and Verify
chmod +x /etc/init.d/derper
rc-update add derper default
rc-service derper start
rc-service derper status
# Check listening ports (replace with actual port numbers)
netstat -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>"
# Local TCP connectivity (replace with DERP port)
nc -zv 127.0.0.1 <DERP_TCP_PORT>
# View process
ps aux | grep derper
# Check kernel logs if startup fails
dmesg | tail -20
Expected output:
tcp 0 0 :::<DERP_TCP_PORT> :::* LISTEN 1234/derper
udp 0 0 :::<STUN_UDP_PORT> :::* 1234/derper
If a line is missing, the corresponding port is not listening. Check the service logs.
Install Tailscale Client
Alpine does not support the official one-line install script (
curl -fsSL https://tailscale.com/install.sh | sh); you must install viaapk.
Ensure /etc/apk/repositories contains the community repository (using Alpine 3.19 as an example):
http://dl-cdn.alpinelinux.org/alpine/v3.19/main
http://dl-cdn.alpinelinux.org/alpine/v3.19/community
If missing, append it:
echo "http://dl-cdn.alpinelinux.org/alpine/v3.19/community" >> /etc/apk/repositories
Install and start:
apk update
apk add tailscale
rc-update add tailscale default
rc-service tailscale start
tailscale up
Copy the https://login.tailscale.com/a/... authentication link from the terminal to your browser to complete login, then run tailscale status to confirm the status.
4.4 Appendix: Cross-Compilation on Another Machine (Low-Memory VPS)
VPS with less than 2GB RAM will OOM during compilation. You can compile on another machine and upload the binary.
# 1) Prepare Go environment (version >= 1.21 recommended)
go version
# 2) Switch download source for mainland China
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
# 3) Set target platform (example: Linux AMD64)
export GOOS=linux
export GOARCH=amd64 # For ARM64, change to arm64
export CGO_ENABLED=0
# 4) Build derper
go install tailscale.com/cmd/derper@latest
ls ~/go/bin/derper
# 5) Create directory on VPS (replace <VPS_IP> with server public IP)
ssh root@<VPS_IP> "mkdir -p /usr/local/derp/{bin,certs}"
# 6) Upload to VPS
scp ~/go/bin/derper root@<VPS_IP>:/usr/local/derp/bin/
# 7) Verify on VPS
ssh root@<VPS_IP>
chmod +x /usr/local/derp/bin/derper
ldd /usr/local/derp/bin/derper # Should output "not a dynamic executable"
/usr/local/derp/bin/derper --version
Compilation parameter notes:
CGO_ENABLED=0: Generates a pure static binary, reducing runtime environment dependenciesGOOS/GOARCH: Target operating system and architecture
After uploading and verifying, return to the corresponding OS section (§4.1 / §4.2 / §4.3) and continue from the “Verify Compilation Result” step to complete certificates, service configuration, and Tailscale installation.
5. Configure ACL (derpMap)
Go to the Tailscale admin console → Access controls → JSON editor, and merge the following derpMap into your existing ACL (do not overwrite other configuration items).
Replace
<REGION_ID>,<REGION_CODE>,<VPS_IP>,<DERP_TCP_PORT>,<STUN_UDP_PORT>in the JSON below with your actual values (see §1).
{
"derpMap": {
"OmitDefaultRegions": false, // Keep official DERP as fallback
"Regions": {
"<REGION_ID>": {
"RegionID": <REGION_ID>, // Custom region ID, recommend 900+
"RegionCode": "<REGION_CODE>", // Custom region code
"RegionName": "Custom DERP",
"Nodes": [
{
"Name": "myderper",
"RegionID": <REGION_ID>,
"HostName": "<VPS_IP>", // Must match certificate and -hostname
"DERPPort": <DERP_TCP_PORT>, // Must match service listening port
"STUNPort": <STUN_UDP_PORT>, // Must match STUN listening port
"CanPort80": false,
"InsecureForTests": true // Required for IP + self-signed certificate scenarios
}
]
}
}
}
}
The Tailscale ACL editor supports HuJSON format, so
//comments can be kept as-is without removal.
Common pitfalls:
- Trailing commas in JSON causing save failures
- ACL ports not matching service listening ports
HostNamenot matching certificate SAN
6. Client Reconnection and Configuration Propagation
After saving the ACL, it is recommended to reconnect clients to ensure the new configuration is pulled:
# Debian / Ubuntu / RHEL / CentOS (systemd)
systemctl restart tailscaled
# Alpine (OpenRC)
rc-service tailscale restart
# Or the universal approach (all systems)
tailscale down
tailscale up
Wait 1-2 minutes before running connectivity tests.
6.1 Enable Peer Relay and Exit Node (Optional)
If you want the DERP node to also serve as a Peer Relay and Exit Node:
Enable Peer Relay
Peer Relay is currently in Beta and requires Tailscale 1.86+. It allows devices in your tailnet to act as high-throughput relay servers. When direct connections aren’t possible, Tailscale tries available Peer Relays first before falling back to DERP servers. Useful for large file transfers, HD streaming, and other high-throughput scenarios behind strict NATs.
Step 1: Enable Peer Relay on the relay device
# Verify version >= 1.86
tailscale version
# Enable Peer Relay on a specified UDP port
tailscale set --relay-server-port=40000
Ensure the UDP port (e.g., 40000/udp) is open in both your firewall and cloud security group, otherwise other devices cannot connect through the Peer Relay.
Step 2: Add a grant policy in ACL
Enabling the port alone is not enough. You must add a Peer Relay grants policy in the Tailscale admin console under Access controls.
First, find your DERP server’s Tailscale IP:
tailscale status
The first column 100.x.x.x is the Tailscale IP.
Append the Peer Relay entry to the grants array in your existing ACL (multiple grants can coexist). Below is a complete working example with basic network access and Peer Relay. Replace <RELAY_HOSTNAME> with the DERP server’s Tailscale IP:
{
"grants": [
{
"src": ["*"],
"dst": ["*"],
"ip": ["*"] // Basic network access, allows all devices to communicate
},
{
"src": ["*"],
"dst": ["<RELAY_HOSTNAME>"], // Replace with your DERP server's Tailscale IP
"app": {
"tailscale.com/cap/relay": [] // Relay capability, no additional parameters needed
}
}
],
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"] // Allow all traffic
}
]
}
Note: Tailscale ACL defaults to denying all traffic. Without
aclsrules or a network access grant (the first"ip": ["*"]entry), devices cannot communicate and the Peer Relay won’t work either. The example above includes both to ensure proper operation.If you already have your own
aclsandgrantsconfiguration, simply append the second Peer Relay grant to the end of your existinggrantsarray.
dst: Must replace<RELAY_HOSTNAME>with the DERP server’s Tailscale IPtailscale.com/cap/relay: Required relay capability declaration
Multiple Peer Relays: If you have multiple Peer Relays or want finer control, you can use tags instead. See Tailscale Peer Relays documentation for details.
Step 3: Verify
# Generate traffic and check status
tailscale status | grep peer-relay
# Or test with ping
tailscale ping <peer-device>
When a connection uses a Peer Relay, tailscale status shows peer-relay instead of direct or relay.
Disable Peer Relay
tailscale set --relay-server-port=""
Enable Exit Node
Linux requires IP forwarding to be enabled before it can act as an Exit Node:
# If your system has /etc/sysctl.d/ directory (recommended)
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
# Otherwise use /etc/sysctl.conf
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p /etc/sysctl.conf
If using firewalld, you also need to allow masquerading:
firewall-cmd --permanent --add-masqueradeAfter enabling IP forwarding, ensure your firewall denies forwarded traffic by default (
ufwandfirewallddo this by default) to prevent routing unintended traffic.
Advertise the Exit Node and enable route acceptance:
sudo tailscale set --advertise-exit-node
sudo tailscale up --accept-routes
Finally, approve the Exit Node from the Tailscale admin console:
- Open the Machines page and locate the device
- Click the
...menu on the device → Edit route settings - Enable Use as exit node
If the device is authenticated by a user with Exit Node permissions configured in ACL
autoApprovers, it will be approved automatically.
6.2 Network Performance Optimization (Optional)
Enabling UDP GRO forwarding and disabling GRO list can improve Tailscale network throughput:
# Enable UDP GRO forwarding
ethtool -K eth0 rx-udp-gro-forwarding on
# Enable UDP segmentation offload
ethtool -K eth0 rx-gro-list off
# Verify settings
ethtool -k eth0 | grep gro
If you get
ethtool: command not found:# Alpine apk add ethtool # Debian / Ubuntu apt install -y ethtool # RHEL / CentOS / Rocky dnf install -y ethtool
7. Testing and Verification
7.1 Quick Test Commands
# Network capability check (UDP / DERP)
tailscale netcheck
# Device and peer node status
tailscale status
# Structured status (useful for debugging and documentation)
tailscale status --json
7.2 tailscale netcheck Key Points
UDP: true: UDP communication is workingNearest DERP: Should be close to your custom regionDERP latency: Lower latency on your self-hosted node is better
7.3 tailscale status Key Points
- Whether your machine shows
active - Whether peer nodes are visible
- Whether the connection path shows
relay "<REGION_CODE>"ordirect ...
7.4 Example Output Interpretation
Output may vary slightly between versions; focus on the interpretation approach.
Example 1: tailscale netcheck
Report:
* UDP: true
* IPv4: yes, 203.0.113.24:41641
* IPv6: no, but OS has support
* MappingVariesByDestIP: false
* HairPinning: false
* Nearest DERP: tyxy
* DERP latency:
- tyxy: 18.2ms
- tok: 62.4ms
Interpretation:
UDP: true: STUN hole-punching capability is workingNearest DERP: tyxy: Client has connected to the custom regionMappingVariesByDestIP: false: Usually indicates more stable NAT mapping
Example 2: tailscale status
100.64.0.2 vps-derp linux active; direct 198.51.100.10:41641, tx 12 rx 20
100.64.0.8 laptop windows active; relay "tyxy", tx 3 rx 6
Interpretation:
active; direct ...: Currently using direct connectionactive; relay "tyxy": Currently relaying through DERP, region istyxy- If all connections remain
relaylong-term, investigate NAT/firewall/port policies
8. Common Issues and Troubleshooting
8.1 OOM During Compilation
Symptom: fatal error: runtime: out of memory or signal: killed.
Solution: Use §4.4 Cross-Compilation on Another Machine, or temporarily add swap.
8.2 Permission denied When Running derper
Symptom: Still getting Permission denied after chmod +x.
Cause: /usr/local may be mounted with noexec.
mount | grep /usr/local
Solution: Copy to another location to run, or remount without noexec:
cp /usr/local/derp/bin/derper /root/derper
chmod +x /root/derper
/root/derper --version
8.3 Port Conflict (address already in use)
# systemd systems (replace with actual port numbers)
ss -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>"
# Alpine (replace with actual port numbers)
netstat -tulnp | grep -E "<DERP_TCP_PORT>|<STUN_UDP_PORT>"
Solution: Stop the conflicting process or change the port, and update both the service parameters and ACL accordingly.
8.4 Custom DERP Not Visible (Certificate/Hostname Issues)
ls -la /usr/local/derp/certs/
# Replace <VPS_IP> with actual IP
openssl x509 -in /usr/local/derp/certs/<VPS_IP>.crt -text -noout | grep -E "Subject:|IP Address"
# Check hostname in service configuration
grep hostname /etc/systemd/system/derper.service # systemd systems
grep hostname /etc/init.d/derper # Alpine (OpenRC)
Solution: Ensure the certificate filename, SAN IP, and -hostname parameter are all consistent.
8.5 Works Locally, Fails Externally (Firewall Issues)
# Local test (replace with DERP port)
nc -zv 127.0.0.1 <DERP_TCP_PORT>
# External test from another machine (replace with actual IP and port)
nc -zv <VPS_IP> <DERP_TCP_PORT>
Solution: Focus on checking cloud security group and system firewall rules.
8.6 ACL Configuration Not Taking Effect
Troubleshooting order:
- Check JSON syntax (especially trailing commas)
- Wait 1-2 minutes for synchronization
- Reconnect client:
tailscale down && tailscale up - Run
tailscale netcheckagain
8.7 Service Script Issues
systemd (Debian/Ubuntu/RHEL/CentOS):
Symptom: Failed to start derper.service or status=203/EXEC.
Solution: Verify ExecStart path is correct and binary has execute permissions; run systemctl daemon-reload after changes.
OpenRC (Alpine):
Symptom: syntax error: unterminated quoted string.
Solution: Delete and recreate using cat << 'EOF' to avoid manual quoting issues.
9. Summary
Key takeaways:
- Docker is quicker to set up; non-Docker is closer to system-level operations — choose based on your needs
- For IP-based scenarios, self-signed certificates +
InsecureForTests: trueis typically the paired configuration HostName, certificate SAN, listening port, and ACL port must be consistent across the entire chain- After configuration, always reconnect clients and verify using
netcheck/status
With this guide, you can deploy, verify, and troubleshoot DERP on Debian/Ubuntu, RHEL/CentOS, Alpine, and other mainstream Linux distributions.