- What is Unbound
- About this Image
- Installation
- How to use this Image
- Known Issues
- Troubleshooting
- Documentation
- Contributing
Unbound is a validating, recursive, caching DNS resolver. It is designed to be fast and lean and incorporates modern features based on open standards. Late 2019, Unbound has been rigorously audited, which means that the code base is more resilient than ever.
Source: unbound.net
This advanced Unbound Docker image is based on Alpine Linux with focus on security, privacy, performance and a small size.
The Unbound process runs in the context of an unpriviledged non-root user, makes use of unprivileged ports (5335 tcp/udp) and the image is built using a secure distroless scratch image.
Unbound is configured as a DNSSEC validating DNS resolver, which directly queries DNS root servers utilizing zone transfers holding a local copy of the root zone (see IETF RFC 8806) as your own recursive upstream DNS server in combination with Pi-hole or AdGuard Home for adblocking in mind, but works also as a standalone server.
There's a really nice explanation at the Pi-hole documentation page of what that means without becoming too technical:
Whom can you trust? Recently, more and more small (and not so small) DNS upstream providers have appeared on the market, advertising free and private DNS service, but how can you know that they keep their promises? Right, you can't. Furthermore, from the point of an attacker, the DNS servers of larger providers are very worthwhile targets, as they only need to poison one DNS server, but millions of users might be affected. Instead of your bank's actual IP address, you could be sent to a phishing site hosted on some island. This scenario has already happened and it isn't unlikely to happen again... When you operate your own (tiny) recursive DNS server, then the likeliness of getting affected by such an attack is greatly reduced.
However, even though the image is intended to run a recursive setup, it does not necessarily mean that it has to be used that way. You are absolutely free to edit the unbound.conf file according to your own needs and requirements*
, especially if you'd rather like to use an upstream DNS server which provides DoT or DoH features.
To provide always the latest stable, hardened and optimized versions per hardware architecture, the following software components are compiled online from source in the build processes of their own dedicated repositories using workflow driven CD pipelines using trusted GitHub Actions:
All components as well as the Internic files (root.hints and root.zone) are verified with their corresponding PGP keys and signature files if available to guarantee maximum security and trust.
When NLnet Labs publishes a new stable Unbound release, the image will be built, pushed to Docker Hub, tagged and released -including the required signing by the bot @madnuttah-bot
according to the repo's strict security policies- to GitHub on a week-daily schedule without sacrificing security measures like SHA256 verification of the downloaded source tarball. As we take your network security serious, we are still able to manually update the image as soon as security fixes of the images' components were released. The same applies to the OpenSSL build environment when an OpenSSL update got released.
Note
We're not manually building release candidates of Unbound anymore, instead there are automated canary builds which will be created from the most recent NLnet Labs Unbound GitHub commit at 20:00 UTC from Monday to Friday if you want to ride on the bleeding edge of the development of Unbound.
The latest
image is scanned for vulnerabilities using the Aqua Security Trivy and Docker Scout vulnerability scan on a daily schedule. If vulnerabilities have been detected, they'll show up in Security. The canary
build only shows the results in the workflow's run details and are being scanned at buildtime. You need to be logged into GitHub to view the logs.
Distroless production and canary multiarch-builds for Linux-based 386, arm/v6, arm/v7, arm64 or amd64 platforms are available on Docker Hub.
Please adapt the /usr/local/unbound/unbound.conf
file and our example docker compose
files to your needs. The docker compose
files also deploy Pi-hole for blocking ads and to prevent tracking but isn't limited to Pi-hole. The image can also be used as an upstream for AdGuard Home or any similar ad blocking solution.
We don't like large, monolithic config files much.
Luckily Unbound can load configs through a include:
clause. To provide a better structuring of the unbound.conf
file, directories for optionally storing zone and other configuration files as well as for your certificates and the unbound.log
file have been created and can be mounted as volumes:
-
/usr/local/unbound/certs.d/
for storing your certificate files. -
/usr/local/unbound/conf.d/
for your configuration files like interfaces.conf, performance.conf, security.conf, etc. -
/usr/local/unbound/log.d/unbound.log
in case you need to access it for troubleshooting and debugging purposes. -
/usr/local/unbound/zones.d/
for your zone configuration files like auth-zone.conf, stub-zone.conf, forward-zone.conf, etc.
Also make sure to uncomment the line include: "/usr/local/unbound/conf.d/*.conf"
of the unbound.conf
.
Warning
The config files in the conf.d
and zones.d
folders must be named with the suffix .conf to prevent issues with specific host configurations.
Don't forget to secure your setup when everything runs.
The splitted configuration files located in doc/examples/usr/local/unbound
are only meant to give you an impression on how to separating and structuring the configs. Please mind that those files are examples which also needs to be edited to make them work for your environment if you intend to use them.
Note
Splitting ain't really necessary as the included default unbound.conf
will perfectly do the job after you adapted the settings to suit your environment. You don't need to bind mount the config folders, just ignore them.
Filesystem
/
βββ entrypoint
βββ usr
β βββ local
β β βββ unbound
β β βββ certs.d
β β β βββ ...
β β βββ conf.d
β β β βββ *.conf
β β βββ iana.d
β β β βββ root.hints
β β β βββ root.key
β β β βββ root.zone
β β βββ log.d
β β β βββ unbound.log
β β βββ sbin
β β β βββ healthcheck.sh
β β β βββ unbound.sh
β β βββ unbound.conf
β β βββ unbound.d
β β β βββ null -> /dev/null
β β β βββ random -> /dev/random
β β β βββ sbin
β β β β βββ unbound
β β β β βββ unbound-anchor
β β β β βββ unbound-checkconf
β β β β βββ unbound-control
β β β β βββ unbound-control-setup
β β β β βββ unbound-host
β β β βββ unbound.pid
β β β βββ urandom -> /dev/urandom
β β βββ zones.d
β β βββ *.conf
β βββ ...
β β
β ...
...
Commands list
. fg sh
: getopts shift
[ grep source
[[ groupmod su-exec
alias hash sys/
awk healthcheck.sh test
bg help times
bin/ history tini
break id trap
cd jobs true
chdir kill type
chgrp let ulimit
chown lib/ umask
command local unalias
continue netstat unbound
dev/ printf unbound-anchor
drill proc/ unbound-checkconf
echo pwd unbound-control
etc/ read unbound-control-setup
eval readonly unbound-host
exec return unset
exit sbin/ usermod
export sed usr/
false set wait
Variable | Default | Value | Description |
---|---|---|---|
TZ |
UTC |
<Timezone> |
Set your local timezone as DNSSEC, logging and the optional redis and statistics rely on precise time |
Variable | Default | Value | Description |
---|---|---|---|
UNBOUND_UID |
1000 |
INT |
Your desired user id for user _unbound |
UNBOUND_GID |
1000 |
INT |
Your desired group id for group _unbound |
HEALTHCHECK_PORT |
5335 |
INT |
The port Unbound uses (only used by the extended healthcheck) |
EXTENDED_HEALTHCHECK |
false |
BOOL |
Set this to true if you want to use the extended healthcheck |
EXTENDED_HEALTHCHECK_DOMAIN |
nlnetlabs.nl |
string |
The domain/host to run the extended healthcheck against |
ENABLE_STATS |
false |
BOOL |
Set this to true if you want to enable Unbound statistics . Please follow the instructions there |
DISABLE_SET_PERMS |
false |
BOOL |
Set this to true and define user _unbound for full rootless mode. The UNBOUND_UID and UNBOUND_GID will both be overridden with 1000 in that case |
Caution
Setting DISABLE_SET_PERMS
to true
without defining user: _unbound
or --user _unbound
will run the container under the root account (which, of course, might be desired). The init screen in the log will show you the user who is running Unbound.
If you ain't sure what this variable does, you'll most likely don't need it.
Port | Description |
---|---|
5335 |
Listening Port (tcp/udp) |
The most elegant way to get started is using docker compose. We have provided combined Pi-hole/Unbound docker compose
samples which we're using in slightly modified form that makes use of a MACVLAN/shim Bridge network which must be adapted to your network environment.
Important
All entries in angle brackets (<...>) need your very attention!
Note
You'll probably want an additional custom bridge network so your host is able communicate with the container and vice versa (for updating the Docker host, etc.). If you don't like to have an additional shim network, take a look at this workaround. We prefer using a combined MACVLAN/Bridge network configuration, but other network configurations will run as well.
If you don't want to run Unbound in recursive mode but rather having it forwarding your queries to an upstream server, make sure your ../iana.d/root.hints
file is up-to-date and change the following lines in your unbound.conf
:
server:
...
for-upstream: no
...
forward-zone:
name: "."
# Forward queries to i.e. quad9
forward-addr: 9.9.9.9
forward-addr: 149.112.112.112
...
This image can also be used as a standalone DNS resolver without Pi-hole or AdGuard Home. The given ports must be changed to 53
(tcp/udp) in your unbound.conf
and docker compose
then. Additionally verify that connections to localhost are allowed (see healthcheck
). You need to enable a capability in your compose file as the _unbound
user only has limited permissions, see issue 54
. You can find more information about runtime privileges and Linux capabilities here.
cap_add:
- NET_BIND_SERVICE
If you use a Synology Diskstation with Container Manager
, take also a look here
.
Anyway, you can also spin up the container with the following command:
docker run --name unbound -d \
-p 5335:5335/udp \
-p 5335:5335/tcp \
--restart=unless-stopped \
madnuttah/unbound:latest
Tip
Even it takes a little more effort, we recommend accessing the CacheDB rather via Unix Socket than via tcp. The speed is superior in comparison to a network connection.
Due to the restricted environment of the image, it's not possible to just map and access the redis server's socket but need to use a "proxy" container which provides access to both containers, unbound
as well as unbound-db
, so there's an additional busybox container providing the socket in an own volume.
Extend your existing docker compose
file's server:
section with the content of this snippet
. The loading order of the modules is also important, cachedb
has to be loaded before iterator
and after validator
.
server:
module-config: "validator cachedb iterator"
Create a new mountpoint like ../unbound-db/
, and place this redis.conf
there.
Create a new entry for cachedb in your unbound.conf
with the content of this cachedb.conf
or put the file in your conf.d
directory if you use the structured directories.
You can verify the connection to redis in the unbound.log
or by typing docker logs unbound
in the shell:
...
Feb 18 22:01:02 unbound[1:0] notice: init module 1: cachedb
Feb 18 22:01:02 unbound[1:0] notice: Redis initializationΒ
Feb 18 22:01:02 unbound[1:0] notice: Connection to Redis established
...
If you like to have a healtheck for this container which we'd recommend strongly, you got our back
. Read on, we'll explain how to set this up in the next heading.
In Portainer you can also view the cachedb.d
volume with a contained redis.sock
file by clicking browse
.
Another way is to connect to the unbound-db
container and monitor the redis-cli
:
Socket:
docker exec -ti unbound-db sh -c "redis-cli -s /usr/local/unbound/cachedb.d/redis.sock monitor"
Network:
docker exec -ti unbound-db sh -c "redis-cli monitor"
The boot order set in your docker compose
is also important. Redis depends on the 'socket server', Unbound depends on Redis. If you use Pi-hole or AdGuard Home, it will depend on Unbound.
For the sake of completeness, you can also flange Unbound to Redis by network. Just follow the given steps, except the part preparing and creating the unbound-db-socket container. Use proper name resolution by whether docker internal resolution, your DNS or use fixed IP addresses and change your cachedb.conf
or your unbound.conf
according to this:
cachedb:
backend: "redis"
redis-server-host: unbound-db # The hostname or IP of your Redis server
redis-server-port: 6379
redis-expire-records: yes
Important
The general use of the healthcheck is optional but highly recommended.
The healthcheck can be enabled and configured quite self-explanatory in your docker compose
file. Check out the example
compose files or the snippet below to get you started; each compose file has got the healthcheck included, the most complete example is the one we use ourselves
.
...
unbound:
...
healthcheck:
test: /usr/local/unbound/sbin/healthcheck.sh
interval: 60s
retries: 3
start_period: 5s
timeout: 15s
...
To enable the healthcheck for your CacheDB (Redis)
server, please define it in your docker compose
file according to this and download it's healthcheck script
and put it in it's persistent volume, make it available in the compose file's volume definition and make the file executable. You will need to restart the Redis container afterwards.
The default healthcheck only checks for opened Unbound ports using netstat and grep. We got asked why we don't include netcat (nc) into the image to actually connect to opened ports, this is the reason.
To enable the extended healthcheck, which uses NLnet Labs' LDNS drill tool to query domains or hosts, please set the healtheck's optional environment variables in your docker compose
file or run command.
Note
The extended healthcheck is deactivated by default in favor of your privacy and security.
To verify that the healthcheck is working and the container is doing what it is supposed to do, consult your Portainer instance or execute docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
in the shell of your Docker host.
Standard healthcheck console output:
yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
β
Port 5335 open
Standard healthcheck console output showing an issue:
yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
β οΈ Port 5335 not open
Extended healthcheck console output:
yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
β
Port 5335 open
β
Domain 'unbound.net' resolved to '185.49.140.10'
Extended healthcheck console output showing an issue:
yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
β
Port 5335 open
β οΈ Domain 'unbound.net' not resolved
Not in the console but rather in Portainer (and here on this page of course) the colored unicode emoji icons will show you the condition of your container at a first glance.
Warning
Even we use it by ourselves to keep less important containers updated, we don't recommend using solutions like Watchtower to update critical services like your production DNS infrastructure automatically.
A notification service like the one in Watchtower or DIUN can inform you when an update has been released so you can take appropriate action if needed.
If you want to manually update to the latest
version available on Docker Hub, just pull the image using docker compose pull
and recreate your container by executing docker compose up -d
.
Pulling the latest image without a compose file can be done by docker pull madnuttah/unbound:latest
.
We also created a companion project
using Zabbix for shipping the Unbound stats via a Zabbix Active Agent
to Grafana without using additional tools like Zabbix Sender using a frankensteined healthcheck script
.
- There's a difference between 'vanilla' Docker and the variant Synology uses. If the container won't spin up when trying to use a privileged port like
53 tcp/udp
you might need to run the container inroot mode
by setting theDISABLE_SET_PERMS
environment variable totrue
without auser
definition or defineuser: root
in the compose file's Unbound service section or your shell command.
Tip
You can run the available commands
directly from the shell of your Docker host like docker exec -ti unbound COMMAND
. To check your Unbound config(s) for errors for example, execute docker exec -ti unbound unbound-checkconf
.
- You'd like to use a different
unbound.conf
than the oneincluded
? No problem, just make sure to change at least the following settings and fix crucial paths, otherwise the container will fail to start:
server:
username="" # Not set in config but compose or command if needed
chroot="" # Distroless, so no chroot necessary
directory="/usr/local/unbound" # This is the folder where Unbound lives in
-
If you have trouble spinning up the container, start it with the minimal config first. Analyze the logs using
docker logs unbound
or yourunbound.log
and fix warnings and errors there. When it runs, attach volumes one by one. Success means to adapt the defaultunbound.conf
to your needs then. -
Most issues take place because there are missing files like the
unbound.log
or due to incorrect permissions on Unbound's volumes. The container won't start up in such cases. Make sure yourUNBOUND_UID/UNBOUND_GID
, default:1000:1000
, (_unbound:_unbound
) has read/write permissions on it's folders. -
When you see log entries like those below or similar issues in the healthcheck, verify that you permit connections to localhost. There are multiple places in the
unbound.conf
where this could be disabled, fromaccess control
toDo-Not-Query-Localhost
and so on. You'll likely need to check the whole file. Isn't that alone one good reason for the concept with the separated config directories? If you can't find the culprit, don't hesitate giving us a shout.
unbound[1:0] fatal error: could not open ports
unbound[1:0] error: can't bind socket: Permission denied for 127.0.0.1 port 53
-
If you see the warning
unbound[1:0] warning: unbound is already running as pid 1
, executingdocker compose down && docker compose up -d
will remove the PID and also the warnings in the log. -
This is no issue and shows that Unbound is doing trust anchor signaling to the root name servers. See this URL for more details.
unbound[0:1] info: generate keytag query _ta-4f66. NULL IN
You'll find a redacted version of the Docker compose stack we`re currently using for comparison purposes here.
In-depth documentation for NLnet Labs Unbound is available on the Unbound documentation website and here goes a direct link to the documentation of the default unbound.conf file.
You have found a bug, got something to make better, have an idea for a shiny new feature or just a question? That's amazing! Feel free to submit an issue or a pull request, we β€οΈ contributions by the open source community.