Skip to content

Examples

The examples/ directory has self-contained configs for specific scenarios. Each file is a complete dnspatch.toml: copy it, replace the CHANGE_ME placeholders and the ${...} references, and start the daemon with --config. CI checks every file with --check-config, so they stay valid. The files below are included from the repository, so what you see is what CI checks.

File Shows
minimal.toml The smallest working config: one retriever, one provider, one instance. Start here.
dual-stack.toml One instance writing both an A and an AAAA record, using two retrievers pinned to ipv4 and ipv6.
family-dual-with-fallback.toml A retriever whose service is itself dual-stack (family = "dual"), plus a fallback chain across further retrievers for the families it does not fill.
interface-ipv6.toml Reading the IPv6 address from a network interface instead of an external service, with a delegated prefix picked by network.
nicru.toml Updating a record at NIC.RU with one request for both addresses; notes the NIC.RU quirk of changing same-name records in every zone of the contract. Other dyndns2 services use type = "dyndns2" with a base_url.
proxy.toml Reaching a provider's API through a proxy, for a DNS host that only accepts requests from a fixed address.
rfc2136.toml Updating a record on your own name server (BIND, Knot, PowerDNS, ...) with RFC 2136 dynamic updates signed with a TSIG key.
secrets-from-files.toml Reading a secret from a file (${file:/path}) instead of an environment variable, the way Docker/Kubernetes secrets are mounted.
multi-provider.toml Several instances and providers in one process: two sites, two DNS hosts, independent polling intervals.
full-build.toml The notifiers, which only exist in the full build, next to ping_url (a ping on every cycle): [notify.<name>] (events published to a broker: status changes by default, more with events, per instance).

For the parameters of every plugin see the Parameter reference; the general syntax is in the Configuration overview.

The files

minimal.toml

# The smallest working config: one retriever, one provider, one instance.
# Start here, copy the file to dnspatch.toml, replace the placeholders, and
# read dnspatch.toml.example (or docs/PARAMETERS.md) once you need a parameter
# that is not shown here.
#
# The retriever is declared inline, with "type" instead of "ref": that is
# only possible because it is used by a single instance. A retriever or
# provider shared by more than one instance needs a named [retriever.<name>]
# or [provider.<name>] block instead — see multi-provider.toml.

[[instance]]
name = "home"

[[instance.retriever]]
type = "ifconfigco"

[[instance.provider]]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${PASSWORD}"

dual-stack.toml

# One instance, one A record and one AAAA record: two retrievers, one per
# family. Which family a retriever reports is decided by the address it
# actually returns, not by this file, so the "family" parameter here only
# tells the retriever which address to ask the service for.

[retriever.v4]
type   = "ifconfigco"
family = "ipv4"

[retriever.v6]
type   = "ifconfigco"
family = "ipv6"

[provider.regru]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${PASSWORD}"

[[instance]]
name = "home"

[[instance.retriever]]
ref = "v4"

[[instance.retriever]]
ref = "v6"

[[instance.provider]]
ref = "regru"

family-dual-with-fallback.toml

# A fallback chain across three retrievers of different roles. icanhazip is
# itself dual-stack, so "family = dual" tries to fill both A and AAAA in one
# call; it is tried first. Only the families it does NOT fill are then asked
# from ipify (pinned to ipv6) and ifconfigco (pinned to ipv4) — a retriever
# pinned to a family that is already filled is skipped without being called.
#
# If icanhazip succeeds for both families, ipify and ifconfigco are never
# called. If it fails outright, ipify is tried for IPv6 and ifconfigco for
# IPv4.

[retriever.icanhazip]
type   = "icanhazip"
family = "dual"

[retriever.ipify]
type   = "ipify"
family = "ipv6"

[retriever.ifconfigco]
type   = "ifconfigco"
family = "ipv4"

[provider.regru]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${PASSWORD}"

[[instance]]
name = "home"

[[instance.retriever]]
ref = "icanhazip"

[[instance.retriever]]
ref = "ipify"

[[instance.retriever]]
ref = "ifconfigco"

[[instance.provider]]
ref = "regru"

interface-ipv6.toml

# Reading the IPv6 address from a network interface instead of asking an
# external service. The address is already on the interface, so nothing
# leaves the host to find it out.
#
# Typical for a home router or server behind an ISP that hands out a
# delegated prefix (DHCPv6-PD): the interface holds an address from that
# prefix, and the prefix changes when the ISP renumbers you.
#
# Of the addresses on the interface, only public ones count: link-local
# (fe80::/10), unique local (fc00::/7) and loopback are skipped. If several
# public addresses remain, for example a stable one and privacy (temporary)
# ones, the numerically lowest is used; the operating system does not tell
# them apart, so pin the choice with "network" when it matters.

[retriever.lan]
type    = "interface"
name    = "eth0"                 # "wan", "br0" on OpenWrt; "Ethernet" on Windows
family  = "ipv6"                 # the default for this retriever
network = "2001:db8:1234::/64"   # optional: only addresses inside this prefix

[provider.regru]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${PASSWORD}"

[[instance]]
name = "home"

[[instance.retriever]]
ref = "lan"

[[instance.provider]]
ref = "regru"

nicru.toml

# Updating a record at NIC.RU (RU-CENTER) through its Dynamic DNS service.
# For another service that speaks the dyndns2 protocol use type = "dyndns2"
# and give it the update URL in base_url.
#
# NIC.RU needs the "Dynamic DNS" service switched on for the domain. The
# update changes the A records with this name in EVERY zone of the contract,
# not only in the one the domain belongs to. Use the login and password of the
# account that is allowed to update it.
#
# Both addresses go in one request (myip and ipv6). A family a retriever did
# not report is left out of the request, and the service may then fill it in
# with the address the request came from, so give the provider the families
# you want kept.

[[instance]]
name = "home"

[[instance.retriever]]
type   = "ifconfigco"
family = "dual"

[[instance.provider]]
type     = "nicru"
hostname = "home.example.com"
username = "my-login"
password = "${PASSWORD}"

proxy.toml

# A provider whose API only accepts requests from a fixed address, reached
# through a proxy on a host with that address. The retriever stays direct: it
# must report the address of THIS host, not the proxy's.
#
# Every provider and every reference to it can set its own "proxy", so
# different zones can leave through different hosts; "proxy = direct" means
# no proxy at all, whatever HTTP_PROXY and HTTPS_PROXY say.

[retriever.home]
type = "ifconfigco"

[provider.regru]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${PASSWORD}"
proxy    = "${PROXY_URL}"

[[instance]]
name = "home"

[[instance.retriever]]
ref = "home"

[[instance.provider]]
ref = "regru"

rfc2136.toml

# Updating a record on your own name server (BIND, Knot DNS, PowerDNS,
# Technitium, ...) with RFC 2136 dynamic updates, the protocol behind
# nsupdate. No third-party DNS service is involved.
#
# The rfc2136 provider is not in the lightweight build: use the -full image or
# binary, or build with -tags rfc2136.
#
# On the server, create a TSIG key (for BIND: tsig-keygen dnspatch-key) and
# allow it to update the record in the zone, for example in named.conf:
#
#   update-policy { grant dnspatch-key name home.example.com. A AAAA; };
#
# Put the base64 secret it prints into the TSIG_SECRET environment variable.
# The server must be the zone's primary (or forward updates to it) and be
# reachable on TCP port 53 from the host dnspatch runs on.

[[instance]]
name = "home"

[[instance.retriever]]
type = "ifconfigco"

[[instance.provider]]
type          = "rfc2136"
server        = "ns1.example.com"
zone          = "example.com"
rr_name       = "home"
key_name      = "dnspatch-key"
key_algorithm = "hmac-sha256"
key_secret    = "${TSIG_SECRET}"

secrets-from-files.toml

# Secrets read from files instead of environment variables: the format
# Docker/Kubernetes secrets use when mounted into a container. Prefer this
# over "${NAME}" when the secret must never show up in "docker inspect" or a
# process's environment.
#
# "${file:/path}" is replaced with the file's contents, minus one trailing
# newline. An unreadable file is an error, same as an unset variable.

[retriever.home]
type = "ifconfigco"

[provider.regru]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${file:./secrets/password.txt}"

[[instance]]
name = "home"

[[instance.retriever]]
ref = "home"

[[instance.provider]]
ref = "regru"

multi-provider.toml

# Several instances and providers in one process: two independent sites,
# each on its own polling interval, one of them writing to two zones at
# two different DNS hosts.

interval = "5m"

[retriever.home]
type = "ifconfigco"

[retriever.office]
type     = "ifconfigco"
base_url = "https://10.0.0.1"

[provider.regru]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${PASSWORD}"

[provider.selectel]
type         = "selectel"
zone         = "example.net"
rr_name      = "home"
project_name = "CHANGE_ME"
account_id   = "CHANGE_ME"
username     = "CHANGE_ME"
password     = "${PASSWORD}"

# A name in the list is the same as a table with only "ref". Both DNS hosts
# are updated from the same address.
[[instance]]
name      = "home"
retriever = ["home"]
provider  = ["regru", "selectel"]

# Same regru definition, a different zone for this instance: an override
# needs a [[instance.provider]] table.
[[instance]]
name      = "office"
interval  = "1m"
retriever = ["office"]

[[instance.provider]]
ref     = "regru"
zone    = "example.org"
rr_name = "office"

full-build.toml

# The notifiers need the full build (the -full image or binary, or a build with
# -tags full, or with the tag of the one backend you use): status changes
# published to a message broker. The lightweight build rejects this file at
# startup instead of ignoring them, so a config that uses a notifier never
# silently does nothing. The ping on every completed cycle works in every build.
#
# Everything else in the file is the same as on the lightweight build.

# Publishes a small JSON event to Redis Pub/Sub, by default whenever an instance
# flips between success and failure: {"event": "status", "severity": "error",
# "instance": "home", "state": "failure", "success": false, "error": "...",
# "time": "..."}. It goes to the channel dnspatch.events.<instance>; change the
# prefix with topic_prefix. Whatever subscribes to it (a bot, a relay service)
# decides what to do with it.
#
# "address" is a redis:// or rediss:// URL; it carries the password and the
# database index, so keep it in the environment, not in the file.
[notify.alerts]
type    = "redis"
address = "${REDIS_URL}"
# topic_prefix = "dnspatch.events."

# Which events it publishes. The default is ["status"]. The others are
# provider_status, retriever_status, ip_change, cycle and lifecycle (an
# instance starting and stopping).
events = ["status", "provider_status", "ip_change"]

# Each [notify.<name>] definition is its own broker connection, shared by the
# instances that list it in their notify key, each with the events of the
# definition or its own (see the instance below). Define more to reach several
# brokers, of different types or of one type with different addresses, for
# example a second Redis for another subscriber. Uncomment to try it, and add
# "backup" to the notify list of the instance below.
# [notify.backup]
# type         = "redis"
# address      = "${REDIS_URL_BACKUP}"
# topic_prefix = "backup.dnspatch."

# RabbitMQ works the same way: "address" is an amqp:// or amqps:// URL, and
# events go to the durable topic exchange "exchange" (dnspatch by default) with
# the routing key dnspatch.events.<instance>. Bind a queue to it, for example
# with the pattern dnspatch.events.#, and the events wait there while the
# consumer is away.
# [notify.queue]
# type     = "rabbitmq"
# address  = "${RABBITMQ_URL}"
# exchange = "dnspatch"
# events   = ["cycle", "lifecycle"]

# MQTT is the same again: "address" is a mqtt://, mqtts://, tcp://, ssl://,
# ws:// or wss:// URL, and events go to the topic dnspatch/events/<instance>
# (the dots of topic_prefix and the instance name become slashes). Subscribe to
# dnspatch/events/# for everything. qos is 1 by default; retain = true makes the
# broker keep the last event of each topic.
# [notify.mosquitto]
# type    = "mqtt"
# address = "${MQTT_URL}"
# qos     = 1
# retain  = false

[retriever.home]
type = "ifconfigco"

[provider.regru]
type     = "regru"
zone     = "example.com"
rr_name  = "home"
username = "CHANGE_ME"
password = "${PASSWORD}"

[[instance]]
name = "home"
# The notifiers this instance publishes to. Without this key it publishes to
# every notifier defined above; [] publishes to none. A name brings the events
# of the definition. To replace them for this instance only, use a
# [[instance.notify]] table with "ref" and "events" instead of this key (the two
# forms cannot be mixed). Nothing else can be overridden there: the connection
# is shared.
notify = ["alerts"]
# Called on every completed cycle (a GET on success, the same URL with /fail
# appended on failure): a dead man's switch for Healthchecks.io or an Uptime
# Kuma push monitor, which alerts when the pings stop arriving. The URL
# usually holds a secret, so it comes from the environment too.
ping_url = "${PING_URL}"

[[instance.retriever]]
ref = "home"

[[instance.provider]]
ref = "regru"