External DNS DigitalOcean Webhook

July 27, 2026 · View on GitHub

A webhook provider for ExternalDNS that manages DNS records in DigitalOcean.

Features

  • Full DigitalOcean DNS API support
  • Automatic retry with exponential backoff for rate limits (429) and server errors (5xx)
  • Configurable retry parameters
  • Graceful error handling with SoftError support
  • Runs as a sidecar container alongside ExternalDNS

Configuration

Environment Variables

VariableRequiredDefaultDescription
DO_TOKENYes-DigitalOcean API token
DO_DOMAIN_FILTERNo-Comma-separated list of domains to manage
DO_DRY_RUNNofalseEnable dry-run mode
DO_API_PAGE_SIZENo200API pagination size
DO_HTTP_RETRY_MAXNo3Maximum HTTP retries
DO_HTTP_RETRY_WAIT_MINNo1sMinimum wait between retries
DO_HTTP_RETRY_WAIT_MAXNo30sMaximum wait between retries
DO_WORKERSNo10Number of concurrent workers used to fetch records
LOG_LEVELNoinfoLog level (debug, info, warn, or error)
LOG_FORMATNotextLog format (text or json)
WEBHOOK_HOSTNo127.0.0.1Webhook API listener host
WEBHOOK_PORTNo8080Webhook API listener port
HEALTH_HOSTNo0.0.0.0Health and metrics listener host
HEALTH_PORTNo8888Health and metrics listener port

Command Line Flags

--log-level      Log level (debug, info, warn, error) [default: info]
--log-format     Log format (text, json) [default: text]
--host           Webhook API server host [default: 127.0.0.1]
--port           Webhook API server port [default: 8080]
--health-host    Health and metrics server host [default: 0.0.0.0]
--health-port    Health and metrics server port [default: 8888]
--dry-run        Enable dry-run mode
--retry-max      Maximum HTTP retries [default: 3]
--retry-wait-max Maximum wait between retries [default: 30s]

Command-line flags take precedence over their corresponding environment variables. The server exposes two separate listeners:

  • Webhook API (/, /records, /adjustendpoints) — bound to 127.0.0.1:8080 by default. These endpoints are consumed only by ExternalDNS and should not be reachable from outside the pod.
  • Health & metrics (/healthz, /metrics) — bound to 0.0.0.0:8888 by default, so probes and Prometheus can reach them without exposing the webhook API.

Deployment

Using Helm

You can deploy ExternalDNS with this webhook using the official ExternalDNS Helm chart.

Create a values.yaml file:

provider:
  name: webhook
  webhook:
    image:
      repository: ghcr.io/amoniacou/external-dns-digitalocean-webhook
      tag: latest
    env:
      - name: DO_TOKEN
        valueFrom:
          secretKeyRef:
            name: digitalocean-credentials
            key: token
      - name: DO_DOMAIN_FILTER
        value: "example.com"
      - name: DO_HTTP_RETRY_MAX
        value: "5"
    args:
      - --port=8080
      - --host=localhost
      - --health-port=8888
      - --health-host=0.0.0.0
      - --log-level=info
    securityContext:
      runAsUser: 65532
      runAsGroup: 65532
      runAsNonRoot: true
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
          - ALL
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8888
      initialDelaySeconds: 10
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /healthz
        port: 8888
      initialDelaySeconds: 5
      periodSeconds: 5

policy: sync
registry: txt
txtOwnerId: my-cluster

extraArgs:
  webhook-provider-url: http://localhost:8080

domainFilters:
  - example.com

sources:
  - ingress
  - crd

Note: ExternalDNS reaches the webhook API over http://localhost:8080 in the same pod. Health checks and metrics use the separate 0.0.0.0:8888 listener. Probes and Prometheus must therefore target port 8888, not the webhook API port.

Install the chart:

helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm upgrade --install external-dns external-dns/external-dns \
  --version 1.21.1 \
  --namespace external-dns \
  --create-namespace \
  -f values.yaml

Kubernetes Deployment (Sidecar) - Manual

apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
spec:
  template:
    spec:
      containers:
        # ExternalDNS container
        - name: external-dns
          image: registry.k8s.io/external-dns/external-dns:v0.20.0
          args:
            - --source=ingress
            - --source=crd
            - --provider=webhook
            - --webhook-provider-url=http://localhost:8080
            - --policy=sync
            - --registry=txt
            - --txt-owner-id=my-cluster
            - --interval=1m

        # DigitalOcean Webhook sidecar
        - name: digitalocean-webhook
          image: ghcr.io/amoniacou/external-dns-digitalocean-webhook:latest
          securityContext:
            runAsUser: 65532
            runAsGroup: 65532
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
          args:
            - --port=8080
            - --host=localhost
            - --health-port=8888
            - --health-host=0.0.0.0
            - --log-level=info
            - --retry-max=5
            - --retry-wait-max=60s
          env:
            - name: DO_TOKEN
              valueFrom:
                secretKeyRef:
                  name: digitalocean-credentials
                  key: token
            - name: DO_DOMAIN_FILTER
              value: "example.com,example.org"
          ports:
            - containerPort: 8080
              name: http-webhook
            - containerPort: 8888
              name: http-health
          livenessProbe:
            httpGet:
              path: /healthz
              port: http-health
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /healthz
              port: http-health
            initialDelaySeconds: 5
            periodSeconds: 5

Secret

apiVersion: v1
kind: Secret
metadata:
  name: digitalocean-credentials
type: Opaque
stringData:
  token: "your-digitalocean-api-token"

Building

Prerequisites

  • Go 1.26+
  • Make
  • GoReleaser (required for building Docker images)

Commands

# Run tests
make test

# Build binary locally (outputs to bin/webhook)
make build

# Build Docker image (uses GoReleaser to prepare artifacts)
make docker-build

# Run locally
DO_TOKEN=your-token make run

Metrics

The webhook exposes Prometheus metrics at http://localhost:8888/metrics by default (the health and metrics listener, separate from the webhook API). These metrics help track interactions with the DigitalOcean API and monitor rate limits.

MetricDescriptionLabels
digitalocean_api_requests_totalTotal number of requests to DigitalOcean APIaction (HTTP method + path)
digitalocean_api_errors_totalTotal number of failed requests (4xx/5xx)action
digitalocean_api_rate_limits_totalTotal number of rate limit hits (HTTP 429)action

Why Webhook Instead of In-Tree Provider?

  1. Independent release cycle - No waiting for upstream ExternalDNS releases
  2. Custom features - Rate limiting, retry logic, enhanced error handling
  3. Better control - Configure retry parameters for your specific needs
  4. Upstream policy - ExternalDNS prefers webhook providers for new/updated providers

Rate Limiting

This webhook uses the built-in retry mechanism from the godo library:

  • Automatically retries on HTTP 429 (rate limit) and 5xx errors
  • Exponential backoff between retries
  • Configurable max retries and wait times

If all retries are exhausted, the error is returned as a SoftError, allowing ExternalDNS to retry on the next reconciliation cycle.

Credits

This project is based on the original in-tree DigitalOcean provider code from ExternalDNS. We have adapted it to run as a standalone webhook provider with enhanced features and independent lifecycle.

License

Apache License 2.0. See LICENSE for details.