Docker Hubโ - Quay.ioโ - bfren ecosystemโ
Unboundโ is a recursive DNS resolver: instead of sending your lookups to a public DNS provider, it asks the root servers and each domain's own name servers directly, and can check the answers with DNSSEC.
It is built on Alpine + S6โ , with Unbound from Alpine's packages, and is configured with environment variables. It works well as the upstream resolver for Pi-holeโ - see the example compose files in this repository.
services:
unbound:
image: quay.io/bfren/unbound:3.1
restart: unless-stopped
ports:
- "127.0.0.1:53:53/tcp"
- "127.0.0.1:53:53/udp"
environment:
- BF_UNBOUND_ENABLE_DNSSEC=1
Run docker compose up -d, then test it from the host:
dig example.com @127.0.0.1
docker-compose.yml in this repository runs Pi-hole with two Unbound containers, and docker-compose-with-proxy.yml puts Pi-hole's admin site behind a proxy. Copy .env alongside them and change the values.
Images are published to Docker Hub (bfren/unbound) and Quay.io (quay.io/bfren/unbound) for linux/amd64, linux/arm/v7 and linux/arm64, with Unbound 1.25.2 on Alpine 3.24.
| Tag | Example | Use when |
|---|---|---|
latest | You always want the newest image | |
MAJOR | 3 | You want updates, but not breaking changes |
MINOR | 3.1 | You want fixes only - recommended |
VERSION | 3.1.16 | You want to pin this image exactly |
unboundX, unboundX.Y | unbound1.25 | You want an Unbound version |
unboundX.Y.Z | unbound1.25.2 | You want an exact Unbound release |
unboundX.Y-VERSION | unbound1.25-3.1.16 | You want to pin both |
| Port | Purpose |
|---|---|
53/udp | DNS |
53/tcp | DNS (large answers and DNS over TCP) |
| Variable | Values | Description | Default |
|---|---|---|---|
BF_UNBOUND_ENABLE_DNSSEC | 0, 1 | Validate answers with DNSSEC, using the root trust anchor (downloaded and kept up to date). Recommended. | 0 |
BF_UNBOUND_PRIVATE_ADDRESS_ENABLE | 0, 1 | Remove private addresses (e.g. 192.168.0.0/16) from public answers, to protect against DNS rebinding. | 0 |
Each of these sets the Unbound option of the same name - see the unbound.conf manualโ for what they do:
| Variable | Unbound option | Default |
|---|---|---|
BF_UNBOUND_VERBOSITY | verbosity | 1 |
BF_UNBOUND_STATISTICS_INTERVAL | statistics-interval | 0 |
BF_UNBOUND_SHM_ENABLE | shm-enable | no |
BF_UNBOUND_SHM_KEY | shm-key | 11777 |
BF_UNBOUND_STATISTICS_CUMULATIVE | statistics-cumulative | no |
BF_UNBOUND_EXTENDED_STATISTICS | extended-statistics | no |
BF_UNBOUND_STATISTICS_INHIBIT_ZERO | statistics-inhibit-zero | yes |
BF_UNBOUND_NUM_THREADS | num-threads | 1 |
BF_UNBOUND_INTERFACE_AUTOMATIC | interface-automatic | yes |
BF_UNBOUND_PREFER_IP6 | prefer-ip6 | no |
BF_UNBOUND_PREFER_IP4 | prefer-ip4 | no |
BF_UNBOUND_OUTGOING_NUM_TCP | outgoing-num-tcp | 10 |
BF_UNBOUND_INCOMING_NUM_TCP | incoming-num-tcp | 10 |
BF_UNBOUND_SO_RCVBUF | so-rcvbuf | 0 |
BF_UNBOUND_SO_SNDBUF | so-sndbuf | 0 |
BF_UNBOUND_EDNS_BUFFER_SIZE | edns-buffer-size | 1232 |
BF_UNBOUND_MAX_UDP_SIZE | max-udp-size | 1232 |
BF_UNBOUND_SCRUB_PROMISCUOUS | iter-scrub-promiscuous | yes |
BF_UNBOUND_DISCARD_TIMEOUT | discard-timeout | 1900 |
BF_UNBOUND_WAIT_LIMIT | wait-limit | 1000 |
BF_UNBOUND_WAIT_LIMIT_COOKIE | wait-limit-cookie | 10000 |
BF_UNBOUND_CACHE_MIN_TTL | cache-min-ttl | 0 |
BF_UNBOUND_CACHE_MAX_TTL | cache-max-ttl | 86400 |
BF_UNBOUND_CACHE_MIN_NEGATIVE_TTL | cache-min-negative-ttl | 0 |
BF_UNBOUND_DO_IP4 | do-ip4 | yes |
BF_UNBOUND_DO_IP6 | do-ip6 | yes |
BF_UNBOUND_DO_UDP | do-udp | yes |
BF_UNBOUND_DO_TCP | do-tcp | yes |
BF_UNBOUND_HIDE_IDENTITY | hide-identity | no |
BF_UNBOUND_HIDE_VERSION | hide-version | no |
BF_UNBOUND_HIDE_HTTP_USER_AGENT | hide-http-user-agent | no |
BF_UNBOUND_HARDEN_SHORT_BUFSIZE | harden-short-bufsize | yes |
BF_UNBOUND_HARDEN_LARGE_QUERIES | harden-large-queries | no |
BF_UNBOUND_HARDEN_GLUE | harden-glue | yes |
BF_UNBOUND_HARDEN_UNVERIFIED_GLUE | harden-unverified-glue | yes |
BF_UNBOUND_HARDEN_DNS_STRIPPED | harden-dnssec-stripped | no |
BF_UNBOUND_HARDEN_BELOW_NXDOMAIN | harden-below-nxdomain | no |
BF_UNBOUND_HARDEN_ALGO_DOWNGRADE | harden-algo-downgrade | no |
BF_UNBOUND_HARDEN_UNKNOWN_ADDITIONAL | harden-unknown-additional | no |
BF_UNBOUND_QNAME_MINIMISATION | qname-minimisation | yes |
BF_UNBOUND_AGGRESSIVE_NSEC | aggressive-nsec | yes |
BF_UNBOUND_USE_CAPS_FOR_ID | use-caps-for-id | no |
BF_UNBOUND_UNWANTED_REPLY_THRESHOLD | unwanted-reply-threshold | 10000000 |
BF_UNBOUND_DO_NOT_QUERY_LOCALHOST | do-not-query-localhost | yes |
BF_UNBOUND_PREFETCH | prefetch | no |
BF_UNBOUND_PREFETCH_KEY | prefetch-key | no |
BF_UNBOUND_DENY_ANY | deny-any | no |
BF_UNBOUND_MINIMAL_RESPONSES | minimal-responses | yes |
BF_UNBOUND_EDE | ede | yes |
BF_UNBOUND_EDE_SERVE_EXPIRED | ede-serve-expired | yes |
See Alpine + S6โ for BF_DEBUG, BF_TZ and the other base image variables.
On every start /etc/unbound/unbound.conf is generated from the environment variables. Unbound listens on port 53 on every IPv4 address in the container, and answers anyone who can reach it - so only publish the port where you mean to.
Before Unbound starts:
BF_UNBOUND_ENABLE_DNSSEC=1, unbound-anchor downloads or updates the root trust anchor (/etc/unbound/root.key)unbound-checkconf checks the configuration - if it is invalid, Unbound doesn't start, and the error is in the logThen Unbound runs under S6 as the unbound user. The container stops if it crashes. The root hints (the addresses of the root servers) are downloaded when the image is built.
The health check (every 30 seconds) looks up cloudflare.com using Unbound itself, so it fails if Unbound can't reach the internet.
./test.nu # builds the image and runs /test
./run.sh # builds the image and opens a shell, with DNSSEC on and Unbound on 127.0.0.1:5353/udp
/test runs this image's tests alongside the base images' tests: the generated unbound.conf (with and without DNSSEC, defaults, settings and private addresses), the paths used, and the installed Unbound version.
Unbound's configuration check failed, or the trust anchor couldn't be downloaded. Check docker logs <container> - a mistyped value in a BF_UNBOUND_* variable is the usual cause.
SERVFAILWith DNSSEC on, SERVFAIL usually means an answer failed validation - often because the container's clock is wrong, or the domain's DNSSEC is broken. Try the lookup with +cd (checking disabled), e.g. dig example.com @127.0.0.1 +cd, to see which.
On many Linux systems systemd-resolved uses port 53. Publish Unbound on a specific address (e.g. "192.168.1.10:53:53/udp"), or use it only from other containers.
So DNS keeps working while one restarts or updates. Pin the second one to an older image version (UNBOUND_PIN in .env) and update it separately.
unbound.conf includes every *.conf file in /etc/unbound/unbound.conf.d, so mount your own files there.
Content type
Image
Digest
sha256:c09443e7eโฆ
Size
29.9 MB
Last updated
9 minutes ago
docker pull bfren/unbound