Sign inSign up

ricariel/unbound

By ricariel

•Updated about 11 hours ago

Unbound DNS resolver image with DoT/DoH forwarders preconfigured

Image
0

50K+

ricariel/unbound repository overview

⁠unbound-docker

A Unbound⁠ DNS resolver in a container, based on Debian 13. Unbound runs as a fully recursive, validating and caching resolver: it walks the DNS hierarchy from the root servers itself instead of forwarding to an upstream provider.

⁠Features

  • Recursive by design: no forwarders are configured, so queries are resolved from the root zone down. Nothing about the query stream is handed to a third party.
  • DNSSEC validation: the trust anchor is fetched with unbound-anchor and the root hints are refreshed on every container start.
  • Logs to stdout: queries, replies and SERVFAILs go to the container log and can be read with docker logs.
  • Hardened defaults: DNS rebinding protection, QNAME minimisation, deny-any, rate limiting, and hidden identity/version.

⁠Konfiguration und eigene Anpassungen

Dieses Image bringt keine eigene /etc/unbound/unbound.conf mit. Die Datei des Debian-Pakets bleibt unangetastet; sie enthaelt nichts ausser:

include-toplevel: "/etc/unbound/unbound.conf.d/*.conf"

Die Einstellungen dieses Images liegen als Drop-in unter /etc/unbound/unbound.conf.d/unbound-docker.conf. Unbound arbeitet den Glob alphabetisch sortiert ab, und bei skalaren Optionen gewinnt der zuletzt gelesene Wert. Daraus ergeben sich drei Schichten, allein ueber die Dateinamen:

DateiHerkunft
remote-control.confDebian-Paket
root-auto-trust-anchor-file.confDebian-Paket
unbound-docker.confdieses Image
zz-*.confAnpassungen pro Deployment

Ein Deployment kann damit jede Einstellung des Images ueberschreiben — es haengt eine zz-*.conf ein, in Kubernetes etwa als ConfigMap per subPath:

# /etc/unbound/unbound.conf.d/zz-mailstack.conf
server:
    do-ip6: no
    prefer-ip6: no

Ziffernpraefixe waeren hier falsch: Ziffern sortieren vor Buchstaben, ein 10-*.conf laege also vor den Paketdateien und wuerde von ihnen ueberschrieben. Daher unbound-docker (nach remote- und root-) und fuer Deployments zz- (nach unbound-).

Zwei Folgen dieser Reihenfolge:

  • control-enable bleibt no, obwohl remote-control.conf aus dem Paket yes sagt — unbound-docker.conf wird danach gelesen.
  • root-auto-trust-anchor-file.conf bleibt wirksam, weil das Image auto-trust-anchor-file nicht selbst setzt. Die DNSSEC-Validierung ist damit aktiv, und docker-entrypoint.sh legt den Anker unter /var/lib/unbound/root.key an — dort erwartet ihn das Paket, waehrend Debians unbound-anchor ohne -a nach /usr/share/dns/root.key schreiben wuerde. Fehlt die Datei, startet Unbound nicht.

⁠Quickstart

docker run -d --name unbound \
  -p 53:53/udp -p 53:53/tcp \
  unbound-docker

The container listens on 0.0.0.0:53 for UDP and TCP.

⁠Startup Sequence

docker-entrypoint.sh runs before Unbound itself:

  1. unbound-anchor fetches or updates the DNSSEC root trust anchor.
  2. The root hints are downloaded from internic.net to /etc/unbound/root.hints.
  3. unbound-control-setup generates the control certificates.
  4. Unbound is started via exec, so it becomes PID 1 and receives signals directly.

Steps 1 and 2 need outbound network access at start time. Without it the container falls back to the root hints baked into the image at build time.

⁠Configuration

The shipped unbound.conf is a complete configuration, not a fragment. To change it, mount your own file over /etc/unbound/unbound.conf.

⁠Access Control

Recursion is only allowed for loopback and the private address ranges (RFC 1918, ULA, IPv6 loopback). Everything else is refused, so the resolver is not an open resolver even if port 53 is exposed accidentally.

⁠Cache and Performance
SettingValue
num-threads3
msg-cache-size~136 MB
rrset-cache-size~272 MB
cache-min-ttl / cache-max-ttl300 s / 86400 s
prefetch / prefetch-keyon
serve-expiredon
serve-expired-ttl2592000 s (30 days)
serve-expired-client-timeout1800 ms
serve-expired-reply-ttl30 s

prefetch refreshes popular entries before they expire, and serve-expired answers from a stale entry while the refresh is in flight — both trade a little freshness for noticeably lower latency.

The three serve-expired-* values turn that from a latency trick into an outage measure. Should the authoritative nameservers for a zone become unreachable, no fallback resolver helps — every resolver asks those same machines. A stale answer is the only thing left, and these decide how long it stays available and how quickly a client gets it:

  • serve-expired-ttl caps how long after expiry an answer may still be served. Set explicitly because the built-in default has differed between Unbound versions, and an outage lasting longer than a day would otherwise still fail. 30 days is measured against a typical RRSIG validity window — beyond it a stale answer would no longer validate anyway.
  • serve-expired-client-timeout is the actual failover switch: hand out the stale answer after 1.8 s and keep trying upstream in the background, instead of making the client wait out the full upstream timeout.
  • serve-expired-reply-ttl keeps stale replies short-lived so clients come back promptly once someone answers again.

The cache sizes assume a host with a few hundred MB to spare. Lower them in your own config if the container is memory-constrained.

⁠Privacy and Hardening
  • qname-minimisation sends only the minimum label set to each authoritative server.
  • use-caps-for-id and unwanted-reply-threshold raise the cost of spoofing attempts.
  • private-address entries block DNS rebinding: public names may not resolve to private addresses.
  • harden-* options reject downgraded, stripped or malformed DNSSEC responses.
  • hide-identity / hide-version suppress CHAOS class fingerprinting.
⁠Logging

verbosity: 1 with query, reply and SERVFAIL logging enabled. This is chatty on a busy resolver — set log-queries: no and log-replies: no in your own config if you only care about failures.

⁠Remote Control

control-enable is no. The entrypoint still generates the control certificates, so enabling unbound-control only requires flipping that one setting.

⁠docker-compose

services:
  unbound:
    image: unbound-docker
    restart: unless-stopped
    ports:
      - "53:53/udp"
      - "53:53/tcp"
    volumes:
      - ./unbound.conf:/etc/unbound/unbound.conf:ro

Mounting the configuration read-only is safe: the entrypoint writes only the trust anchor, the root hints and the control certificates, none of which live in that file.

⁠Building

docker build -t unbound-docker .

The image is based on Debian 13 and adds unbound, unbound-anchor, curl, bash, openssl, ldnsutils and tzdata.

⁠Contributing

This project uses pre-commit for style and quality checks, Conventional Commits for the history, and Renovate for dependency updates.

⁠License

MIT — see LICENSE⁠.

Tag summary

Content type

Image

Digest

sha256:6adc29dad…

Size

68.4 MB

Last updated

about 11 hours ago

docker pull ricariel/unbound