DERP (Designated Encrypted Relay for Packets) is the relay and connectivity component of Tailscale. It primarily handles two tasks:

  1. Assisting nodes with NAT traversal (hole-punching) to establish direct connections
  2. 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> with xxx.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 logs
  • max-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 logs
  • max-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 the docker-compose-plugin binary 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 logs
  • max-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 via apk.

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 derper binary; 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

--version output like 1.xx.x-ERR-BuildInfo is normal (go install builds 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 -hostname parameter. It’s recommended to use <VPS_IP>.crt and <VPS_IP>.key directly.

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 verification
  • Restart=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

--version output like 1.xx.x-ERR-BuildInfo is normal (go install builds 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 -hostname parameter. It’s recommended to use <VPS_IP>.crt and <VPS_IP>.key directly.

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 verification
  • Restart=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

--version output like 1.xx.x-ERR-BuildInfo is normal (go install builds 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 -hostname parameter. It’s recommended to use <VPS_IP>.crt and <VPS_IP>.key directly.

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 verification
  • supervisor=supervise-daemon + respawn_delay=5: Auto-restart on crash (equivalent to systemd’s Restart=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 via apk.

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 dependencies
  • GOOS / 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
  • HostName not 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 acls rules 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 acls and grants configuration, simply append the second Peer Relay grant to the end of your existing grants array.

  • dst: Must replace <RELAY_HOSTNAME> with the DERP server’s Tailscale IP
  • tailscale.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-masquerade

After enabling IP forwarding, ensure your firewall denies forwarded traffic by default (ufw and firewalld do 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:

  1. Open the Machines page and locate the device
  2. Click the ... menu on the device → Edit route settings
  3. 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 working
  • Nearest DERP: Should be close to your custom region
  • DERP 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>" or direct ...

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 working
  • Nearest DERP: tyxy: Client has connected to the custom region
  • MappingVariesByDestIP: 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 connection
  • active; relay "tyxy": Currently relaying through DERP, region is tyxy
  • If all connections remain relay long-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:

  1. Check JSON syntax (especially trailing commas)
  2. Wait 1-2 minutes for synchronization
  3. Reconnect client: tailscale down && tailscale up
  4. Run tailscale netcheck again

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: true is 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.