Install with Docker
The image is ghcr.io/operomtl/opdns-node, for linux/amd64 and
linux/arm64. It contains the static node binary and Alpine’s Unbound,
which the node starts and supervises. It runs as an unprivileged user that
may bind port 53. Data lives in the volume at /var/lib/opdns.
Tags: each release is tagged with its version (1.2.3, 1.2, v1.2.3),
and latest points at the newest stable release. Pin a version for
anything you care about.
Free port 53
Section titled “Free port 53”On many Linux distributions systemd-resolved listens on
127.0.0.53:53, which conflicts with publishing port 53 on all addresses.
Either publish on the LAN address only (-p 192.168.1.10:53:53/udp), or
turn off its stub listener:
[Resolve]DNSStubListener=nothen sudo systemctl restart systemd-resolved.
Run it
Section titled “Run it”The dashboard’s Nodes → Add a node dialog shows this command with your enrolment code filled in (see Enrol a node):
docker run -d --name opdns-node --restart unless-stopped \ -p 53:53/udp -p 53:53/tcp \ -p 127.0.0.1:8053:8053 \ -v opdns-node:/var/lib/opdns \ -e OPDNS_ENROL_TOKEN=<enrolment-code> \ ghcr.io/operomtl/opdns-node:<version>services: opdns-node: image: ghcr.io/operomtl/opdns-node:<version> restart: unless-stopped ports: - "53:53/udp" - "53:53/tcp" - "127.0.0.1:8053:8053" volumes: - opdns-node:/var/lib/opdns environment: OPDNS_ENROL_TOKEN: <enrolment-code>volumes: opdns-node:docker compose up -dThe node installs a list artifact only if its Ed25519 signature verifies.
You do not need to configure the key: by default
(lists.trust_cloud_key: true) an enrolled node learns it from the cloud
on every profile pull and keeps it in lists_keys.json in the volume, so
it can verify lists after a restart without the internet. To pin keys
yourself instead, set OPDNS_LISTS_PUBLIC_KEYS and
OPDNS_LISTS_TRUST_CLOUD_KEY=false
(list signing keys).
OPDNS_ENROL_TOKEN is used once, on first start; after that the node’s
identity is in the volume and the variable is ignored. It is safe to leave
in the Compose file, but the code is single-use and expires after 15
minutes anyway.
A container started with neither an enrolment code, an existing identity
in its volume, nor a standalone profile does not serve DNS: it prints
how to enrol and exits with status 2. With --restart unless-stopped it
keeps restarting and printing the same message until you add the code.
Check it
Section titled “Check it”docker logs opdns-nodeshows the enrolment and “loaded” lines.docker exec opdns-node opdns-node statusprints mode, node id, profile, link state and versions.- Open
http://127.0.0.1:8053/on the host for the status page. dig @<host-address> example.comanswers.- The dashboard’s Nodes page shows the node online.
The image has a health check on /healthz; docker ps shows it as
healthy once Unbound answers and a profile and lists are loaded.
Point your network at it
Section titled “Point your network at it”Set your router’s DHCP DNS server (option 6, and IPv6 RDNSS if you use IPv6) to the node’s LAN address. Queries from every device then go to the node.
Encrypted DNS on the node
Section titled “Encrypted DNS on the node”The node listens for plain DNS by default. For DNS-over-TLS, DoH, DoQ or DoH3 on your LAN, give it a certificate for a name your devices use and set the listeners, for example:
ports: - "853:853/tcp" - "443:443/tcp" volumes: - opdns-node:/var/lib/opdns - /etc/letsencrypt/live/dns.home.example:/certs:ro environment: OPDNS_LISTEN_DOT: ":853" OPDNS_LISTEN_DOH: ":443" OPDNS_LISTEN_TLS_CERT: /certs/fullchain.pem OPDNS_LISTEN_TLS_KEY: /certs/privkey.pem OPDNS_LISTEN_DNS_DOMAIN: dns.home.exampleRenewal needs no restart. The node checks the certificate and key files
every second and loads a new pair as soon as it changes; a pair that fails
to load is logged and the previous certificate stays in use. Connections
already open keep the certificate they started with. docker kill -s HUP opdns-node forces a reload (and a fresh profile and list pull, and
applies the reloadable configuration keys). The log
says certificate reloaded with the new expiry date.
Every key is on the configuration reference.