A self-hosted Tailscale DERP relay using a long-lived self-signed certificate, pinned by SHA256 in the tailnet's derpMap. No ACME, no renewal, no port 80/443 dependency.
For servers that can't expose 80/443 (ISP blocks, China hosts without ICP filing, etc.), HTTP-01 / TLS-ALPN-01 challenges are unusable. DNS-01 works but means running and renewing an ACME pipeline forever. Self-signed + SHA256 pinning sidesteps all of that:
- DERP traffic is already end-to-end encrypted by WireGuard/Noise; TLS is just transport wrapping. Pinning by hash is cryptographically equivalent to a CA-signed cert for this purpose.
- Tailscale parses
CertName: "sha256-raw:<hex>"in the derpMap and verifies the leaf cert hash. Mechanism shipped in tailscale v1.78 (commit 87546a5, Dec 2024). Both derper and clients must be ≥ v1.78.
- Docker + Compose
- OpenSSL on the host (for
gen-cert.sh) - A Tailscale auth key
- Admin access to https://login.tailscale.com/admin/acls
Before first start, configure the tailnet policy and create the auth key — both required by --advertise-tags=tag:relay in docker-compose.yml. See Peer relay below for the ACL snippet and key creation steps. Then:
cp example.env .env
$EDITOR .env # set DOMAIN and TS_AUTHKEY
./gen-cert.sh # 10-year self-signed cert; prints sha256 + derpMap snippet
# → paste the snippet into the tailnet policy at /admin/acls, merging with any existing derpMap
docker compose up -d --build
docker exec tailscaled tailscale set --relay-server-port=40000 # one-time, persists in ts_stateVerify on any tailnet client (≥ v1.78):
tailscale netcheck # your custom region should appear with a sensible RTT
tailscale debug derp 900 # replace 900 with your RegionIDIf the server can't reach Docker Hub or build from source, build on a workstation and ship a tar:
# Workstation
docker compose build # → myderper/derper:v1.96.5
docker pull tailscale/tailscale:v1.96.5
docker save -o images.tar \
myderper/derper:v1.96.5 \
tailscale/tailscale:v1.96.5
scp images.tar user@server:/path/to/myderper/
# Server
docker load -i images.tar
docker compose up -d --no-build./gen-cert.sh --force # regenerates cert; new SHA256 printed
# → update CertName in the tailnet policy
docker compose restart derperCert is good for 10 years; rotation is only needed if the key is compromised or you want to bump the curve.
443/tcpand9443/tcp— both mapped to derper's internal 443; pick whichever your network allows and set it asDERPPortin the derpMap.3478/udp— STUN.40000/udp($PEER_RELAY_PORT) — peer relay listener (see below).
This sidecar tailscaled doubles as a peer relay (UDP-based relay tried before DERP, when tailscale clients can't directly reach each other). DERP traffic itself is unaffected — peer relay only handles regular WireGuard peer-to-peer fallback. Requires tailscale ≥ 1.86 on the relay and on clients that want to use it.
Setup:
- In https://login.tailscale.com/admin/acls add:
{ "tagOwners": { "tag:relay": ["autogroup:admin"] }, "grants": [ { "src": ["*"], "dst": ["tag:relay"], "app": {"tailscale.com/cap/relay": [{}]} } ] } - Create the auth key at https://login.tailscale.com/admin/settings/keys with
tag:relaypre-authorized. Use it asTS_AUTHKEY. - Make sure
$PEER_RELAY_PORT(default 40000/udp) is open in your host firewall and reachable from the public internet for best results. - After the first
docker compose up, apply the relay port (already in the Setup steps above). The pref persists in thets_statevolume — only re-run if you wipe the volume.
Verify with tailscale ping <peer> from a client (≥ v1.86). When peer relay is in use you'll see lines like:
pong from peer (100.x.y.z) via peer-relay(<server-ip>:40000:vni:1) in 23ms
If you see via DERP(...) instead, either direct P2P succeeded (good — no relay needed) or both peer-relay attempts failed (check the host firewall on $PEER_RELAY_PORT).
--hostname=127.0.0.1indocker-compose.ymlis intentional. derper'smanualCertManageronly skips SNI validation (allowing thesha256-raw:...SNI cert-pinning clients send) when--hostnameparses as an IP. The user-facing address lives in the derpMap'sHostNamefield, decoupled from this internal flag.- Cert files are named
certs/127.0.0.1.{crt,key}to match the above. The cert's CN/SAN still embedDOMAINfor human readability — pinning doesn't care about subject fields. - derper is built from upstream
tailscale.com/cmd/derper(derper/Dockerfile), not a third-party image. Build args:DERPER_REF,GOPROXY,GOSUMDB.
docker-compose.yml two services: tailscaled (userspace, verify-clients sidecar) + derper
derper/Dockerfile upstream derper, multi-stage build
gen-cert.sh cert generator + fingerprint printer
example.env env template (real .env is gitignored)
certs/ generated cert/key (gitignored)