Sign inSign up

gugang2006/tailscale-derper

By gugang2006

•Updated about 2 months ago

Multi-arch Tailscale DERP with HTTP/HTTPS, acme.sh, optional STUN, and dynamic-IP policy GitOps.

Image
0

511

gugang2006/tailscale-derper repository overview

⁠DERP container with ACME renewal and dynamic-IP policy GitOps

This project builds Tailscale's derper from upstream source and packages the repository-root derper-linux-amd64 or derper-linux-arm64 binary with a pinned copy of acme.sh. It can terminate TLS directly or run as a certificate-free HTTP DERP behind an external TLS terminator. It can also detect changing public IP addresses and update one custom DERP region in a HuJSON tailnet policy stored in a Git repository. GitHub CLI (gh) is included for API, authentication, and PR workflows. The automatic policy sync supports either Git over SSH with a deploy key or GitHub HTTPS authentication with GH_TOKEN.

Prebuilt multi-architecture images are published on Docker Hub as gugang2006/tailscale-derper⁠, with linux/amd64 and linux/arm64 variants.

⁠Build from the latest Tailscale source

Clone the Tailscale source tree and this project as sibling directories. The commands below use the Go toolchain pinned by Tailscale, so a separately installed matching Go release is not required.

Prerequisites: Git, Docker with Buildx, and network access to GitHub, Docker Hub, the Go toolchain download service, and an Alpine package mirror.

mkdir tailscale-derper-build
cd tailscale-derper-build

git clone https://github.com/tailscale/tailscale.git
git clone https://github.com/yeabow/tailscale-derper.git

Compile both Linux architectures from the current Tailscale checkout and place the output binaries in this project's build context:

cd tailscale

git pull --ff-only origin main

TS_USE_TOOLCHAIN=1 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
  ./build_dist.sh --strip \
  -o ../tailscale-derper/derper-linux-amd64 ./cmd/derper

TS_USE_TOOLCHAIN=1 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
  ./build_dist.sh --strip \
  -o ../tailscale-derper/derper-linux-arm64 ./cmd/derper

eval "$(TS_USE_TOOLCHAIN=1 ./build_dist.sh shellvars)"

cd ../tailscale-derper

VERSION_SHORT is used as the immutable image tag, while VERSION_LONG is stored in the OCI image metadata and checked against derper -version during the build. This keeps the source revision, binary, and image version in sync.

For a local single-architecture image, select the platform matching the target machine and use --load:

docker buildx build \
  --platform linux/amd64 \
  --load \
  --build-arg DERPER_VERSION="$VERSION_LONG" \
  -t "tailscale-derper:$VERSION_SHORT" \
  -t tailscale-derper:latest \
  .

To publish both architectures, log in to Docker Hub, set your image repository, and use --push:

docker login

IMAGE=your-dockerhub-user/tailscale-derper

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --push \
  --build-arg DERPER_VERSION="$VERSION_LONG" \
  -t "$IMAGE:$VERSION_SHORT" \
  -t "$IMAGE:latest" \
  .

If the default Alpine CDN is slow or unavailable, select a compatible mirror without editing the Dockerfile, for example by adding --build-arg ALPINE_MIRROR=https://mirrors.aliyun.com/alpine.

The Dockerfile accepts linux/amd64 and linux/arm64 targets. It selects and verifies the matching DERP binary and pinned acme.sh release, and builds and tests the HuJSON policy helper.

⁠Pure HTTP DERP without ACME or policy Git

Set DERP_MODE=http to run only the main DERP HTTP listener. In this mode no hostname, ACME account, certificate, Git repository, GitHub key, or token is required. ACME_ENABLED=auto resolves to disabled, and policy sync is disabled by default. The following example also disables STUN, so the process opens only TCP port 8080:

docker run -d \
  --name derper \
  --restart unless-stopped \
  --network host \
  -e DERP_MODE=http \
  -e DERP_PORT=8080 \
  -e STUN_ENABLED=false \
  -e ACME_ENABLED=false \
  -e POLICY_SYNC_ENABLED=false \
  -v derper-state:/var/lib/derper \
  gugang2006/tailscale-derper:latest

Probe it with:

curl -fsS http://127.0.0.1:8080/derp/probe

The standalone Compose file provides the same minimal mode:

DERP_PORT=8080 docker compose \
  -f compose.http.yaml \
  up -d

Standard Tailscale DERP clients use HTTPS. Expose this HTTP-only mode through a correctly configured TLS terminator, or use it only on a trusted internal/test network. To publish the container directly as a custom DERP, use HTTPS mode.

⁠Run HTTPS with ACME HTTP-01

The hostname must already resolve to the host. Allow inbound TCP 80 and 443 and UDP 3478. TCP 80 must remain available for the standalone ACME challenge.

docker run -d \
  --name derper \
  --restart unless-stopped \
  --network host \
  -e DERP_HOSTNAME=relay.example.com \
  -e DERP_PORT=443 \
  -e STUN_PORT=3478 \
  -e [email protected] \
  -v derper-state:/var/lib/derper \
  -v derper-acme:/var/lib/acme \
  gugang2006/tailscale-derper:latest

On Linux, host networking avoids an extra container NAT layer for DERP and STUN. If host networking is unavailable, publish the selected TCP DERP_PORT, UDP STUN_PORT, and TCP port 80 for HTTP-01.

The included compose.yaml provides the same host-networked setup. For this HTTPS mode, set DERP_HOSTNAME and ACME_EMAIL in the environment before running it.

⁠DNS-01

Set ACME_CHALLENGE=dns and ACME_DNS_PROVIDER to an acme.sh DNS provider name, then pass that provider's credential environment variables. The provider type and parameters are passed directly to acme.sh, so any DNS API implemented under acme.sh's dnsapi directory is supported.

Cloudflare example:

-e ACME_CHALLENGE=dns \
-e ACME_DNS_PROVIDER=dns_cf \
-e CF_Token="$CF_Token"

Alibaba Cloud DNS example:

-e ACME_CHALLENGE=dns \
-e ACME_DNS_PROVIDER=dns_ali \
-e Ali_Key="$Ali_Key" \
-e Ali_Secret="$Ali_Secret"

ACME_DNS_SLEEP optionally overrides DNS propagation wait time. Keep provider credentials outside the image and restrict access to the container configuration. DNS-01 is particularly useful with a dynamic public IP because certificate issuance and renewal do not require the hostname's A record to point at the current address.

⁠Dynamic public IP and policy Git synchronization

Enable this feature with POLICY_SYNC_ENABLED=true. At startup the container:

  1. Detects the public IPv4 address using three HTTPS endpoints. Requests bypass HTTP proxies by default so the proxy's egress address is not mistaken for the DERP address.
  2. Rejects private, carrier-grade NAT, link-local, loopback, benchmark, and documentation addresses. When endpoints disagree, a unique majority wins; a tie aborts the update.
  3. Clones the configured branch into an ephemeral directory, parses the policy as HuJSON, and adds or replaces only derpMap.Regions[POLICY_DERP_REGION_ID].
  4. Commits and pushes only when that policy file changed. A rejected concurrent push causes a fresh clone and retry.

It repeats IP detection every POLICY_SYNC_INTERVAL seconds (five minutes by default). It only clones the repository again when the detected address or DERP endpoint settings changed. A transient detection or Git failure leaves the existing remote policy untouched and DERP online; set POLICY_SYNC_REQUIRED=true to make the initial synchronization mandatory.

⁠GitHub SSH deploy key

Example using a GitHub deploy key with write access:

docker run -d \
  --name derper \
  --restart unless-stopped \
  --network host \
  -e DERP_HOSTNAME=relay.example.com \
  -e DERP_PORT=8443 \
  -e STUN_PORT=3479 \
  -e [email protected] \
  -e ACME_CHALLENGE=dns \
  -e ACME_DNS_PROVIDER=dns_cf \
  -e CF_Token="$CF_Token" \
  -e POLICY_SYNC_ENABLED=true \
  -e [email protected]:example/tailnet-policy.git \
  -e POLICY_GIT_BRANCH=main \
  -e POLICY_FILE=policy.hujson \
  -e POLICY_DERP_REGION_ID=900 \
  -e POLICY_DERP_REGION_CODE=home \
  -e 'POLICY_DERP_REGION_NAME=Home Dynamic DERP' \
  --mount type=bind,src=/secure/github-deploy-key,dst=/run/secrets/github_ssh_key,readonly \
  -v derper-state:/var/lib/derper \
  -v derper-acme:/var/lib/acme \
  gugang2006/tailscale-derper:latest

The key must be unencrypted because the service is non-interactive. The image uses GitHub's published SSH host keys with strict host-key checking. For GitHub Enterprise or another SSH Git service, mount its verified known_hosts file and set POLICY_GIT_KNOWN_HOSTS_FILE to that in-container path. A base64 key in POLICY_GIT_SSH_KEY_B64 is also supported, but a read-only Docker secret is preferred because environment variables are easier to expose accidentally.

⁠GitHub token over HTTPS

The built-in policy sync can also use GH_TOKEN when POLICY_GIT_REPO is an HTTPS URL whose host matches GH_HOST. It configures gh as Git's credential helper without writing the token into the repository URL or Git config. A fine-grained token needs read and write access to repository contents, and any branch rules must allow the token owner to push.

Mounting a token file is preferred over putting the token directly in the container environment:

docker run -d \
  --name derper \
  --restart unless-stopped \
  --network host \
  -e DERP_HOSTNAME=relay.example.com \
  -e [email protected] \
  -e POLICY_SYNC_ENABLED=true \
  -e POLICY_GIT_REPO=https://github.com/example/tailnet-policy.git \
  -e GH_HOST=github.com \
  -e GH_TOKEN_FILE=/run/secrets/github_token \
  --mount type=bind,src=/secure/github-token,dst=/run/secrets/github_token,readonly \
  -v derper-state:/var/lib/derper \
  -v derper-acme:/var/lib/acme \
  gugang2006/tailscale-derper:latest

Passing -e GH_TOKEN="$GH_TOKEN" directly is also supported. gh gives GH_TOKEN precedence over its stored credentials. For GitHub Enterprise, configure GH_HOST and use the token variable appropriate for that host.

The updater never replaces the whole policy and never edits grants, acls, ssh, groups, or another DERP region. If derpMap or Regions is absent, it creates that container object; otherwise it changes only the configured region ID. POLICY_OMIT_DEFAULT_REGIONS=preserve (the default) also leaves the current default-region setting untouched.

The Git push only changes the repository. Configure Tailscale's GitOps workflow for that repository separately so accepted commits are applied to the tailnet. With Compose, add the included overlay:

docker compose \
  -f compose.yaml \
  -f compose.policy.yaml \
  up -d

That overlay is the SSH deploy-key example. For HTTPS token authentication, use compose.policy-token.yaml instead and set GITHUB_TOKEN_PATH to the host path of the token file.

docker compose \
  -f compose.yaml \
  -f compose.policy-token.yaml \
  up -d

⁠Renewal behavior

The supervisor runs acme.sh --cron every six hours by default. acme.sh copies renewed material into:

  • /var/lib/derper/certs/<hostname>.crt
  • /var/lib/derper/certs/<hostname>.key

Its persisted reload command writes a marker under /run; PID 1 validates the new files and restarts only the DERP child process. Persist both /var/lib/acme and /var/lib/derper, otherwise the ACME account, certificates, or DERP node identity will be lost when the container is replaced.

To test issuance against the Let's Encrypt staging service, set ACME_STAGING=true. Staging certificates are not trusted by clients.

To use externally managed certificates in HTTPS mode, set ACME_ENABLED=false and mount the certificate and key at the two paths above before starting the container. In HTTP mode, certificate files are neither required nor read.

Useful variables:

VariableDefaultPurpose
DERP_MODEhttpshttps for TLS or http for a certificate-free main listener
DERP_HOSTNAMErequired for HTTPSTLS hostname and certificate filename; optional in HTTP mode
DERP_PORTautoMain DERP port: 443 for HTTPS or 80 for HTTP unless explicitly set
STUN_ENABLEDtrueEnable the built-in UDP STUN listener; set false for DERP-only mode
STUN_PORT3478Public and local UDP STUN port; written to policy
ACME_ENABLEDautoEnable acme.sh; auto enables it for HTTPS and disables it for HTTP
ACME_EMAILrequired with ACMEACME account contact
ACME_SERVERletsencryptacme.sh CA alias or directory URL
ACME_CHALLENGEstandalonestandalone or dns
ACME_KEY_LENGTHec-256ECC or RSA certificate key type
ACME_CRON_INTERVAL21600Renewal check interval in seconds
ACME_DNS_PROVIDERrequired for DNS-01acme.sh provider type, such as dns_cf or dns_ali
ACME_DNS_SLEEPprovider defaultDNS propagation wait override in seconds
DERP_HTTP_PORT-1Secondary HTTP listener in HTTPS mode; must remain -1 in HTTP-only mode
DERP_HEALTHCHECK_PORTDERP_PORTOptional local HTTP/HTTPS health-check port override
POLICY_SYNC_ENABLEDfalseEnable public-IP detection and policy Git sync
POLICY_SYNC_INTERVAL300Public-address recheck interval in seconds
POLICY_GIT_REPOrequired with syncSSH, HTTPS, or file Git repository URL
POLICY_GIT_BRANCHmainBranch to clone and push
POLICY_FILEpolicy.hujsonRelative policy path inside the repository
POLICY_GIT_SSH_KEY_FILE/run/secrets/github_ssh_keyRead-only deploy-key secret path
GH_TOKENunsetGitHub token used by gh and HTTPS policy Git authentication
GH_TOKEN_FILEunsetRead-only token file; its first line overrides GH_TOKEN for policy sync
GH_HOSTgithub.comGitHub hostname that must match the HTTPS policy repository host
POLICY_DERP_REGION_ID900Managed custom region; must be 900 through 999
POLICY_DERP_REGION_CODEdynamicRegion code written to policy
POLICY_DERP_REGION_NAMEDynamic DERPRegion display name written to policy
POLICY_DERP_NODE_NAME<code>-<id>Unique node name written to policy
POLICY_OMIT_DEFAULT_REGIONSpreservePreserve, enable, or disable default DERP regions
DERP_IPV4autoDetect, disable (none), or pin the policy IPv4 value
DERP_IPV6noneDetect (auto), disable, or pin the policy IPv6 value
PUBLIC_IPV4_URLSthree built-insComma-separated plain-text IPv4 endpoints
PUBLIC_IPV6_URLStwo built-insComma-separated plain-text IPv6 endpoints
PUBLIC_IP_TIMEOUT10sTimeout per public-IP endpoint

Additional container arguments are appended to the DERP command line.

Tailscale's current documentation requires custom DERP servers to have direct internet access and warns against placing them behind NAT, a firewall doing address translation, or a load balancer. Dynamic-address automation reduces policy drift but does not remove those protocol and source-address requirements.

Tag summary

Content type

Image

Digest

sha256:8168e2724…

Size

47.8 MB

Last updated

about 2 months ago

docker pull gugang2006/tailscale-derper