自建 Tailscale DERP 中继节点(Docker Compose)
跨德国和中国做远程开发时,Tailscale 有时会长期走公共 DERP 中继,延迟高而且不稳定。要改善这种链路体验,第一步往往不是调 SSH,而是自己补一台更靠近实际网络路径的 DERP 节点。
这篇文章是给未来的自己看的,也给需要自建 Tailscale DERP 的人做一个可复制的参考:在一台小型 VPS 上,用 Docker Compose 跑一个 derper 容器,支持:
- Let’s Encrypt 自动签证书(80/443 开放时)
- 或手动证书(80 被机房拦截时)
- 通过 Tailscale Admin Console 或 Headscale 把自建 Region 下发给客户端(单节点或多节点)
如果还没有自己的 Tailnet,可以先看用 Docker Compose 自建 Headscale。Headscale 负责设备注册和配置下发,DERP 只在设备无法直连时转发加密流量。
2026-08-03 更新:原配置使用第三方
latest镜像,并只挂载单个tailscaled.sock文件。tailscaled重启重建 runtime 目录后会留下 stale bind mount,导致--verify-clients拒绝客户端,而netcheck的 HTTPS 探测可能仍显示正常。本文已改为同 revision 自行构建镜像、挂载整个 runtime 目录、由 systemd 让容器随tailscaled重建(见第 4、5 节;已在两台 VPS 上验证)。
2026-08-30 更新:新增 6.3:某个官方 DERP Region 故障时,Headscale 可以只排除该 Region,不必关闭全部官方 DERP。
0. 适用环境与整体思路
系统要求
- Ubuntu 22.04+ 或 Debian 12+
- 1C / 1G 也能跑(低负载场景足够)
依赖条件
- 一台有公网 IPv4 的 VPS(建议开放 80/TCP、443/TCP、3478/UDP)
- 一个域名,例如:
derp.example.com,A 记录指向该 VPS - 一个已有的 Tailnet,可以使用 Tailscale 官方控制服务器或自建 Headscale
root或可sudo的账号
推荐方案
- 优先使用:Docker Compose +
letsencrypt自动证书 - 若 80/TCP 无法使用,再切换到 manual + 自备证书
如果要跑多个 DERP 节点,建议为每台机器准备独立子域名,例如:
derp.example.com→ Region hk1derphk.example.com→ Region hk2
1. 安装 Docker(含 Compose)
发行版仓库的 docker.io 与 Docker 官方仓库的 docker-compose-plugin 不要混用,这里统一使用 Docker 官方 apt 仓库。完整说明见官方文档:Ubuntu 安装 Docker Engine、Debian 安装 Docker Engine。
先移除发行版自带的旧包(没装过的包会提示找不到,不影响后续步骤):
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove -y $pkg
done
添加 Docker 官方 apt 仓库(Debian 系统把下面两处 ubuntu 替换成 debian):
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
安装 Docker Engine 和 Compose 插件:
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker-ce 安装后默认已配置开机自启。验证:
docker --version
docker compose version
2. 安装并登录 Tailscale(宿主机)
derper 如果启用 --verify-clients,需要访问宿主机的 tailscaled.sock,因此宿主机必须运行 tailscaled 并加入你的 Tailnet。Tailscale 的官方说明还有一个容易漏掉的硬约束:开启客户端验证时,derper 与同机 tailscaled 必须从同一个 Git revision 构建。
curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable --now tailscaled
sudo tailscale up --ssh=false # 按提示在浏览器登录
检查状态:
systemctl is-active tailscaled
tailscale version
tailscale status | head -n 5
记下 tailscale version 输出中的 tailscale commit。后面的 TAILSCALE_REF 必须使用这个完整 commit,而不是只写一个看起来相同的版本号。
2.1 关闭 relay VPS 的自动安装更新
专门承担 DERP 的 VPS 不应让 tailscaled 单独自动升级。否则宿主机先换到新 revision,容器里的 derper 仍是旧 revision,即使 systemd 正确重建了容器,--verify-clients 的版本约束仍然被破坏。
只在这些 relay VPS 上关闭自动安装:
sudo tailscale set --auto-update=false
这不会关闭更新检查。可以用下面的输出确认配置:
tailscale debug prefs | grep -A3 '"AutoUpdate"'
预期为:
"AutoUpdate": {
"Check": true,
"Apply": false
}
Check: true 仍会提示存在新版本,Apply: false 只是不再自动安装。不要把这条设置扩大到所有普通 Tailscale 客户端;这里只是因为 DERP 与同机 tailscaled 必须成对升级。
3. 防火墙与安全组端口放行
需要同时在云厂商“安全组”和系统防火墙(如 UFW)中放行:
80/TCP(Let’s Encrypt HTTP-01;如果后面只用 manual,可不开放)443/TCP(DERP TLS)3478/UDP(STUN)
示例(UFW):
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
sudo ufw enable
4. 固定 revision 构建镜像
创建持久化目录和 compose 目录:
# 身份密钥 & ACME 缓存
sudo mkdir -p /opt/derper-data
# compose 项目
sudo mkdir -p /opt/derper && cd /opt/derper
新建 /opt/derper/Dockerfile。下面的 Go 基础镜像是 2026-08-03 验证时使用的版本;如果将来 Tailscale 提高最低 Go 版本,应随所选 revision 一起更新:
FROM golang:1.26.5-bookworm AS builder
ARG TAILSCALE_REF
RUN test -n "$TAILSCALE_REF"
RUN CGO_ENABLED=0 go install tailscale.com/cmd/derper@"$TAILSCALE_REF" \
&& CGO_ENABLED=0 go install tailscale.com/cmd/derpprobe@"$TAILSCALE_REF" \
&& CGO_ENABLED=0 go install tailscale.com/cmd/stunc@"$TAILSCALE_REF"
FROM debian:bookworm-slim
ARG TAILSCALE_REF
LABEL org.opencontainers.image.source="https://github.com/tailscale/tailscale" \
org.opencontainers.image.revision="$TAILSCALE_REF"
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /go/bin/derper /usr/local/bin/derper
COPY --from=builder /go/bin/derpprobe /usr/local/bin/derpprobe
COPY --from=builder /go/bin/stunc /usr/local/bin/stunc
ENTRYPOINT ["/usr/local/bin/derper"]
再新建 /opt/derper/docker-compose.yml。替换域名,并把 TAILSCALE_REF 替换成前一步记录的完整 commit:
services:
derper:
image: local/derper:tailscale-matched
build:
context: .
args:
TAILSCALE_REF: "<tailscaled-git-commit>"
container_name: derper
restart: unless-stopped
ports:
- "80:80/tcp" # Let's Encrypt HTTP-01
- "443:443/tcp" # DERP TLS
- "3478:3478/udp" # STUN
command:
- "--hostname=derp.example.com"
- "--certmode=letsencrypt"
- "--certdir=/var/lib/derper/certs"
- "--a=:443"
- "--http-port=80"
- "--stun=true"
- "--stun-port=3478"
- "--verify-clients=true"
- "--socket=/var/run/tailscale/tailscaled.sock"
- "--c=/var/lib/derper/derper.key"
security_opt:
- "no-new-privileges:true"
volumes:
- /opt/derper-data:/var/lib/derper
- /var/run/tailscale:/var/run/tailscale:ro
/var/lib/derper 需要持久化,里面包含 derper.key、ACME 缓存和证书。这样容器重建不会频繁重新签证书,也能保持 DERP 身份稳定。
socket 则挂载整个 /var/run/tailscale 目录,而不是单独挂载 socket 文件;ro 保证容器不能修改宿主机 runtime 目录。
先构建镜像,不要在 revision 不匹配时直接启动:
cd /opt/derper
docker compose build --pull derper
docker image inspect local/derper:tailscale-matched \
--format '{{ index .Config.Labels "org.opencontainers.image.revision" }}'
5. 让容器跟随 tailscaled 重建
只写 restart: unless-stopped 不够。它能在容器退出时重启容器,但 tailscaled 单独重启时,DERP 容器不会退出,旧 bind mount 仍可能留在原 inode。
新建 /etc/systemd/system/derper-compose.service:
[Unit]
Description=DERP container synchronized with tailscaled runtime
Requires=docker.service tailscaled.service
After=docker.service tailscaled.service network-online.target
Wants=network-online.target
PartOf=tailscaled.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/derper
ExecStart=/usr/bin/docker compose up -d --force-recreate --no-build derper
ExecStartPost=/usr/bin/docker exec derper test -S /var/run/tailscale/tailscaled.sock
ExecStop=/usr/bin/docker compose stop derper
TimeoutStartSec=120
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
PartOf=tailscaled.service 会把对 tailscaled 的 stop/restart 传播给这个 unit;--force-recreate 让 Docker 重新建立 bind mount;ExecStartPost 则在容器看不到 socket 时直接让启动失败,而不是留下一个表面运行、实际拒绝客户端的 DERP。
启用服务:
sudo systemctl daemon-reload
sudo systemctl enable --now derper-compose.service
5.1 基础校验
检查端口监听情况:
# 80 / 443
ss -lntup | grep -E ':80|:443'
# 3478
ss -lnup | grep -E ':3478'
查看日志:
docker compose logs --tail=120
# 预期能看到类似 “serving on :443 with TLS” 的输出
docker exec derper test -S /var/run/tailscale/tailscaled.sock
systemctl is-active derper-compose.service
验证 TLS 证书是否正常(Issuer 是否为 Let’s Encrypt,CN/SAN 是否包含你的域名):
openssl s_client -connect derp.example.com:443 -servername derp.example.com -brief </dev/null
以上只能说明进程、端口、TLS 和 socket 基本正常。真正的 DERP 协议还要在已加入同一 tailnet 的客户端上测试:
tailscale debug derp 901
把 901 换成自己的 Region ID。预期结果应包含成功建立 DERP 连接,而不只是 HTTPS latency 成功。
5.2 成对升级 Tailscale 与 derper
第 5 节的 unit 解决的是 daemon 重启后 runtime socket inode 变化,不会自动下载源码、构建新版 derper。因此“可以自由重启”不等于“可以单独升级 Tailscale”。
双 DERP 节点应逐台升级。维护其中一台时,另一台和默认 DERP 仍可提供中继。每台按以下顺序执行:
-
停止这台机器的 DERP unit,但保持 unit enabled:
sudo systemctl stop derper-compose.service -
用正常的软件包或 Tailscale CLI 更新宿主机客户端:
sudo tailscale update tailscale version -
复制新的完整
tailscale commit到 Compose 的TAILSCALE_REF。如果tailscale version显示的 Go 版本高于 Dockerfile 基础镜像,也同步更新 Go 镜像版本。 -
先构建和核对 image label,再恢复服务:
cd /opt/derper docker compose build --pull derper docker image inspect local/derper:tailscale-matched \ --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' sudo systemctl start derper-compose.service -
在 VPS 上检查 socket 和日志,再从另一台 tailnet 客户端测试完整 DERP 协议:
# 在 VPS 上 docker exec derper test -S /var/run/tailscale/tailscaled.sock docker compose logs --tail=120 # 在另一台 tailnet 客户端上 tailscale debug derp <RegionID>
确认第一台正常后再升级第二台。安全更新也应及时按这个流程完成,而不是因为关闭自动安装就长期停留在旧版本。
6. 把自建 DERP 下发给客户端
使用 Tailscale 官方控制服务器时,在 Admin Console 中配置 derpMap;使用 Headscale 时,在 config.yaml 中加载本地 DERP Map。下面 6.1 和 6.2 是官方控制台的写法。Headscale 用户在 derp.yaml 中配置自建 Region 的方式见文末 Headscale 文档链接;6.3 单独讲 Headscale 下排除故障官方 Region 的做法。
进入 Admin Console → Access Controls,在 ACL JSON 中添加 derpMap 字段。
控制台支持带注释 JSON(
jsonc),如果提示语法错误,可以先去掉注释再保存。
6.1 单节点示例
"derpMap": {
"OmitDefaultRegions": false,
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "hk1",
"RegionName": "derp_1c1g",
"Nodes": [
{
"Name": "1",
"RegionID": 901,
"HostName": "derp.example.com"
}
]
}
}
}
6.2 双节点示例(新旧两台 VPS)
"derpMap": {
"OmitDefaultRegions": false,
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "hk1",
"RegionName": "derp_1c1g",
"Nodes": [
{ "Name": "1", "RegionID": 901, "HostName": "derp.example.com" }
]
},
"902": {
"RegionID": 902,
"RegionCode": "hk2",
"RegionName": "derp_2c4g",
"Nodes": [
{ "Name": "1", "RegionID": 902, "HostName": "derphk.example.com" }
]
}
}
}
保存之后,一般 1–3 分钟内会下发到客户端。
客户端验证(任意一个节点):
tailscale netcheck
# 预期能看到 hk1 / hk2 的 Region,且有延迟(UDP=true 更好)
多台 VPS 共用同一份 Dockerfile 和 compose 模板,但每台必须分别把 TAILSCALE_REF 改为本机 tailscale version 输出的 commit,不能只改 --hostname。连通性可用 tailscale ping <任意节点的 100.x IP> 验证。
6.3 Headscale:只排除一个故障的官方 Region
Headscale 可以同时加载 Tailscale 默认 DERP Map、本地自建 Region 和内置 DERP。遇到客户端反复报告某个官方 Region 不可用时,先确认问题是否只发生在这个 Region(以下命令需要 jq;没有的话直接看 tailscale debug derp-map 原始输出):
tailscale debug derp-map | jq '.Regions["20"]'
tailscale debug derp 20
第一条命令列出 Region 20 的实际节点,第二条测试完整 DERP 协议。还要分别检查这些节点的 IPv4、IPv6 和 TLS;443/TCP 能建立连接,只能证明端口可达,不能证明 TLS 和 DERP 数据通道可用。
如果其他官方 Region 和自建 Region 均正常,可以在 Headscale 加载的 derp.yaml 中只排除这个 Region。按Headscale 部署文章的目录结构,先创建宿主机文件 /opt/headscale/headscale/derp.yaml:
regions:
# Disabled 2026-08-30: TCP/443 was reachable, but TLS was reset on every
# Region 20 node over IPv4 and IPv6; custom regions remained reachable.
20: null
Headscale 容器默认只挂载了 config.yaml 和 data 目录,容器内不存在 /etc/headscale/derp.yaml。在 /opt/headscale/compose.yaml 的 headscale 服务 volumes 中追加一条只读挂载:
volumes:
- ./headscale/config.yaml:/etc/headscale/config.yaml:ro
- ./headscale/derp.yaml:/etc/headscale/derp.yaml:ro
- ./headscale/data:/var/lib/headscale
config.yaml 仍然保留默认 DERP Map,并通过容器内路径加载本地文件:
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
paths:
- /etc/headscale/derp.yaml
重建容器让新挂载生效:
cd /opt/headscale
sudo docker compose up -d
然后从客户端确认 Region 20 已消失,而其他官方 Region 和自建 Region 仍在:
tailscale debug derp-map | jq -r \
'(.Regions | has("20")), (.Regions | keys | join(","))'
tailscale debug derp <自建RegionID>
不要把 urls 一并删除。那样会移除全部官方 DERP,回退流量只能全部走自己的服务器;自建节点停机或客户端到自建节点的路径异常时,会少一层可用的回退。
外置 derper 和 Headscale 内置 DERP 可能部署在同一台 VPS,例如外置 Region 901 与内置 Region 999。它们由不同服务提供,可以使用不同域名、证书和 STUN 端口,因此不是重复配置;但它们共享同一台主机和同一条公网链路,也不能算跨主机冗余。维护期间可以同时保留,不能因为两个 Region 都在线就认为这台 VPS 已经没有单点问题。
Headscale 官方文档也使用 regions: <RegionID>: null 排除单个 Region,并建议在移除默认 DERP 前先验证自建 DERP 的连通性。具体配置见 Customize DERP map。
7. 无法使用 80/TCP 时:切换到 manual 证书模式
部分机房对 80/TCP 有限制,导致 Let’s Encrypt HTTP-01 失败。
这种情况下可以切换到 manual 模式,自行管理证书。
7.1 准备证书
准备好域名证书和私钥(CN/SAN 包含 derp.example.com):
derp.example.com.crtderp.example.com.key
放到宿主机 /usr/local/cert:
sudo mkdir -p /usr/local/cert
sudo cp derp.example.com.crt derp.example.com.key /usr/local/cert/
sudo chmod 700 /usr/local/cert
sudo chmod 600 /usr/local/cert/*
7.2 修改 Docker Compose
在 docker-compose.yml 中:
command:
- "--hostname=derp.example.com"
- "--certmode=manual"
- "--certdir=/cert"
- "--a=:443"
- "--http-port=80"
- "--stun=true"
- "--stun-port=3478"
- "--verify-clients=true"
- "--socket=/var/run/tailscale/tailscaled.sock"
- "--c=/var/lib/derper/derper.key"
volumes:
- /opt/derper-data:/var/lib/derper
- /usr/local/cert:/cert:ro
- /var/run/tailscale:/var/run/tailscale:ro
这时可以不开放 80/TCP,但 443/TCP 和 3478/UDP 必须可达。
重启并再次验证:
sudo systemctl restart derper-compose.service
docker compose logs --tail=120
openssl s_client -connect derp.example.com:443 -servername derp.example.com -brief </dev/null
8. 常见问题与排查思路
1)443 端口占用
日志中出现 bind: address already in use:
ss -lntup | grep -E ':443'
# 找到占用进程,停止后再启动 derper 容器
2)tailscale netcheck 看不到自建 Region,或 Region 延迟为空
- 检查 Admin Console 中
derpMap的 JSON 是否保存成功、无语法错误; - 等待策略下发,并确认客户端网络能访问 DERP 域名的 443 端口;
- 延迟为空只表示这次 latency probe 没测出来,不能单独证明 DERP 离线;
- 用
tailscale debug derp <RegionID>测完整 DERP 协议,再结合服务端日志判断。
3)UDP: false
- 说明客户端所在网络的 UDP 打洞能力较差;
- DERP 仍然可以工作,只是部分连接会走 TCP;
- 如果能控制网络环境,尽量保证客户端网络出站UDP可用。
4)Let’s Encrypt 申请失败
在日志中看到 acme/autocert 相关错误,或关于 HostWhitelist 的警告时:
- 检查域名解析是否正确指向当前 VPS;
- 检查 80/TCP 是否可从外网访问;
- 确认
--hostname与访问时的域名 / SNI 一致; - 如果机房不放行 80/TCP,直接切
manual模式即可。
5)--verify-clients 报 socket 不存在或连接失败
先分别检查宿主机和容器看到的 runtime 目录:
stat -Lc '%d:%i %n' /var/run/tailscale /var/run/tailscale/tailscaled.sock
docker exec derper stat -Lc '%d:%i %n' \
/var/run/tailscale /var/run/tailscale/tailscaled.sock
如果宿主机 socket 存在、容器内不存在,或容器 mountinfo 出现 //deleted,这是 stale bind mount,不是权限问题。立即恢复可用:
cd /opt/derper
docker compose up -d --force-recreate --no-build derper
持久化修复仍是第 5 节的 systemd unit。不要用 chmod 666 掩盖问题;它既不能换掉旧 inode,又让所有本机用户都能访问 socket。只有日志明确是 permission denied 时,才应检查容器 UID/GID 或组权限。
还要核对镜像 label 中的 revision 与 tailscale version 输出一致;版本号接近但 revision 不同,仍不满足官方对 --verify-clients 的要求。
6)频繁重建容器触发 LE 频率限制
- 确保
/var/lib/derper已挂载到宿主机:- /opt/derper-data:/var/lib/derper - 尽量不要在测试阶段频繁清空该目录。
9. Peer Relay 与 DERP 的分工
Tailscale 1.86 以后可以把 tailnet 内的设备配置成 Peer Relay。连接顺序是:直连、Peer Relay、DERP。Peer Relay 适合无法直连、又需要较低延迟或较高吞吐的固定设备;Peer Relay 不可用时,连接仍可使用 DERP。
在有公网 UDP 入口的 VPS 上启用:
sudo tailscale set --relay-server-port=40000
sudo ufw allow 40000/udp
然后在 tailnet policy 中授权。src 是需要通过 Peer Relay 被访问的固定设备,dst 是充当 Peer Relay 的 VPS;公开模板建议使用精确 tag,不要默认放大到整个 tailnet:
{
"src": ["tag:china-fixed"],
"dst": ["tag:peer-relay"],
"app": {
"tailscale.com/cap/relay": []
}
}
所有相关节点都要使用 Tailscale 1.86 或更高版本。产生实际流量后检查:
tailscale ping <目标设备>
tailscale status | grep peer-relay
直连随后建立、输出保持 direct 是正常结果,说明 Peer Relay 只是回退路径;只有直连失败且状态明确显示 peer-relay,才算验证了这条回退路径。
10. 迁移与备份
10.1 备份身份与 ACME 缓存
sudo tar -C /opt -czf derper-data-backup.tgz derper-data
10.2 迁移到新机器
- 在新机器上按前文步骤安装 Docker、Tailscale、防火墙规则;
- 将旧机器的
/opt/derper-data拷贝到新机器同一路径; - 把域名 A 记录切换到新 IP;
- 按前文步骤启动 compose 并验证。
11. 权限与安全建议
-
持久化目录中包含私钥和证书,建议最小权限:
sudo chown -R root:root /opt/derper-data /usr/local/cert sudo chmod 700 /opt/derper-data /usr/local/cert sudo find /usr/local/cert -type f -exec chmod 600 {} \;
日常排查优先看两处:
docker compose logs- 客户端的
tailscale debug derp <RegionID>与tailscale netcheck
扩容更多 Region 时,按同样的模板复制 compose、替换域名、匹配当地 tailscaled revision,再在 derpMap 里加 Region。日常重启由 systemd 自动处理,版本升级则必须逐台、成对完成。
实现细节以 Tailscale 官方的 cmd/derper README 为准;其中明确写了同 revision、端口和 derpprobe 监控要求。客户端自动更新开关和手动更新方式见 Update Tailscale。