Unbound-DOCKERIZED
Unbound - a validating, recursive, and caching DNS resolver. DOCKERIZED!
Disclaimer: I am just the maintainer of this docker container, I did not write the software. Visit the Official Unbound Website or the Official Unbound Github Repository to thank the author(s)! :)
Unbound is a validating, recursive, and caching DNS resolver. It is designed to be fast and lean and incorporates modern features based on open standards.
Some of the core features of Unbound are:
- DNSSEC: Validates the authenticity and integrity of DNS responses.
- Caching: Stores DNS responses to increase speed for subsequent requests and reduce the load on upstream servers.
- Highly Configurable: Offers a wide range of options to fine-tune its behaviour.
- DNS-over-TLS (DoT) & DNS-over-HTTPS (DoH): Encrypts your DNS queries to protect your privacy and prevent eavesdropping.
Official Website: https://nlnetlabs.nl/projects/unbound/about/
Official Github Repository: https://github.com/NLnetLabs/unbound
Docs: https://unbound.docs.nlnetlabs.nl/en/latest/
My Github Repository: https://github.com/Bleala/Unbound-DOCKERIZED
Docker Hub: https://hub.docker.com/r/bleala/unbound
Github Container Registry: https://github.com/Bleala/Unbound-DOCKERIZED/pkgs/container/unbound
I built this image based on Alpine Linux to keep it as slim as possible.
There will always be two different versions:
| Tag | Content |
|---|---|
| latest | Contains the latest stable version |
| x.x.x | Contains the Unbound and Alpine versions mentioned at the bottom of the page and in the release notes |
I am using semantic versioning for this image. For all supported architectures there are the following versioned tags:
- Latest
- Major (1)
- Minor (1.0)
- Patch (1.0.0)
There are also several platforms supported:
Platforms:
- linux/amd64
- linux/arm64
- linux/arm/v7
To ensure the authenticity and integrity of my images, all bleala/unbound images pushed to Docker Hub and GitHub Container Registry (and maybe more in the future) are signed using Cosign.
I use a static key pair for signing. The public key required for verification, cosign.pub, is available in the root of this GitHub repository:
- Public Key:
cosign.pub
You can verify the signature of an image to ensure it hasn't been tampered with and originates from me.
-
Install Cosign: If you don't have Cosign installed, follow the official installation instructions: Cosign Installation Guide.
-
Obtain the Public Key: Download the
cosign.pubfile from this repository or clone the repository to access it locally. -
Verify the Image: Use the
cosign verifycommand. It is highly recommended to verify against the image digest (e.g.,sha256:...) rather than a mutable tag (likelatestor1.23.0). You can find image digests on Docker Hub or GitHub Container Registry.# Ensure 'cosign.pub' is in your current directory, or provide the full path to it. # Replace <registry>/bleala/unbound@sha256:<image-digest> with the actual image reference and its digest. # Example for an image on Docker Hub: cosign verify --key cosign.pub docker.io/bleala/unbound@sha256:<ACTUAL_IMAGE_DIGEST_HERE> # Example for an image on GitHub Container Registry: cosign verify --key cosign.pub ghcr.io/bleala/unbound@sha256:<ACTUAL_IMAGE_DIGEST_HERE>
For instance, to verify the
devtag with the following digestsha256:94aa316e0aa6a845f8e275946df1280c589d3b2104c08dc3ae838eb208b9aed7:cosign verify --key cosign.pub docker.io/bleala/unbound@sha256:94aa316e0aa6a845f8e275946df1280c589d3b2104c08dc3ae838eb208b9aed7
A successful verification will output information like this:
cosign verify --key cosign.pub docker.io/bleala/unbound@sha256:94aa316e0aa6a845f8e275946df1280c589d3b2104c08dc3ae838eb208b9aed7 Verification for index.docker.io/bleala/unbound@sha256:94aa316e0aa6a845f8e275946df1280c589d3b2104c08dc3ae838eb208b9aed7 -- The following checks were performed on each of these signatures: - The cosign claims were validated - Existence of the claims in the transparency log was verified offline - The signatures were verified against the specified public key [{"critical":{"identity":{"docker-reference":"index.docker.io/bleala/unbound"},"image":{"docker-manifest-digest":"sha256:94aa316e0aa6a845f8e275946df1280c589d3b2104c08dc3ae838eb208b9aed7"},"type":"cosign container image signature"},"optional":{"Bundle":{"SignedEntryTimestamp":"MEQCIH8kniH21V+t7h1kjuUVUQZLpJcuYmIfKri5Iu81HCxMAiB2a1eRVOHT3oxfGsRilbuoQoVPCakzZ7nS10p6dkRGWg==","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzODdkZmY3NTUxOWExNTI2MzY4NTVlZjA1YjRhZjUwY2FkYjVlMjU2N2M5NDFhN2VhZmE3NDdmMGM2NmFlMDM2In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQ3R2L0Y1bUo2U0hBODdzVTNrNnYwQmhLb0dIUUJUNEtocTVFL1V0VmZ2bUFpQW9QNy96VHcwN2ZIYkU4NGt0RmdoaitaK0Z3L2NyVGZ1NU1MRFVBU3NEV2c9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGU0VWWFRFYzVjVVI2VFdGdlJ6TlJTSGxXTUhoVFRVZzNRblF3VGdvMVRVWkRNWEV3VFhabE5DOHZVMmwxZVZWbU5VRnBaRVJZY2s5S1kwaEdSalYxZERWUVMyNVViMUZ6YjNWNWRGVTBXVmhoWlM5bU1UQlJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=","integratedTime":1748639173,"logIndex":226072545,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}}}]
To start the container you can run the following
docker run -d -p 53:53/udp -p 53:53/tcp \
-v /path/to/your/config:/etc/unbound/unbound.conf \
bleala/unbound:latestBut since docker compose is easier to maintain, I'll give you a valid docker compose example.
---
networks:
unbound:
name: unbound
driver: bridge
services:
# Unbound - a validating, recursive, and caching DNS resolver. DOCKERIZED!
# https://hub.docker.com/r/bleala/unbound
# https://github.com/Bleala/Unbound-DOCKERIZED
unbound:
image: bleala/unbound:latest
container_name: unbound
hostname: unbound
restart: always
security_opt:
- no-new-privileges:true
environment:
# Optional: set your timezone, for correct container and log time, default to Europe/Vienna
TZ: Europe/Vienna
# Optional: set different Unbound configuration file location, default to /etc/unbound/unbound.conf
UNBOUND_CONFIG: /etc/unbound/unbound.conf
# Optional: set diffenrent Unbound root.key file location, default to /var/unbound/root.key
UNBOUND_ROOT_FILE: /var/unbound/root.key
env_file:
- path: .env
required: false
networks:
unbound: {}
ports:
- target: 53
published: 53
protocol: tcp
mode: host
- target: 53
published: 53
protocol: udp
mode: host
volumes:
- type: bind
source: /path/to/unbound.conf
target: /etc/unbound/unbound.conf
read_only: trueYou can start the docker-compose.yml with the following command
docker compose up -dIf you want to see the container logs, you can run
docker compose logs -for
docker logs -f unboundIf you do not mount your own unbound.conf inside the container, Unbound will use the default unbound.conf I provided.
The default config will just act as a DNS forwarder and will send your DNS requests to Cloudflare using DNS over TLS.
In this projects GitHub repository, you will find the unbound.conf (unbound.conf) that can serve as a starting point and which is the default unbound.conf, if you do not provide your own.
You can also mount a local configuration file named unbound.conf into the container (/etc/unbound/unbound.conf) to use your own Unbound configuration.
You can find all possible configuration options in the official Unbound documentation.
If you prodive your own unbound.conf I suggest that you include the following config:
# User to drop privileges to after binding ports.
# This must match the user created in your Dockerfile.
username: "unbound"
# Unbound's working directory. Used for relative paths if chroot is enabled.
# If chroot is disabled, this is where Unbound might look for some files by default.
directory: "/var/unbound"
# Chroot: path to chroot the server to.
# Empty string "" means no chroot, which simplifies path management in Docker.
chroot: "/var/unbound"
# PID file location.
# Ensure the directory (/var/unbound) is writable by the 'unbound' user
# if Unbound manages the pidfile after dropping privileges.
# (Typically, root creates it before dropping privileges).
pidfile: "unbound.pid"
# File with the trusted root key for DNSSEC validation.
# This file is typically created/updated by the `unbound-anchor` utility.
# Your Dockerfile runs unbound-anchor to create this.
auto-trust-anchor-file: "root.key"
As Unbound is compiled with the corresponding flags (see Dockerfile) during the build process, you should add this config.
The default runtime directory for Unbound inside the container is /var/unbound.
Attention: A faulty unbound.conf can lead to a complete breakdown of DNS resolution in your network. Test your configuration carefully!
I recommend to test your configuration directly with the container like this:
docker run --rm --name unbound-check-config \
--volume /path/to/unbound.conf:/etc/unbound/unbound.conf \
--entrypoint /files/scripts/checkconf.sh \
bleala/unbound:latest
If you get a response like this unbound-checkconf: no errors in /etc/unbound/unbound.conf, your unbound.conf is valid.
And maybe set logfile: "/dev/stdout" in your Unbound configuration for the first startup, so you see all Unbound logs in the container logs.
Once the container is running, you can configure your router or your devices to use the Docker hosts IP address as their DNS server.
I built Unbound with the CacheDB option, so you can use Redis/Valkey as in memory cache for Unbound.
Add Redis or Valkey to your docker-compose.yml and include the following in your unbound.conf:
server:
module-config: "validator cachedb iterator"
cachedb:
backend: "redis"
redis-server-host: redis
redis-server-port: 6379
redis-expire-records: yes
The Unbound container has a dedicated user called unbound inside, which you can (not have to) use within your unbound.conf.
If you set username: "unbound" in your unbound.conf the root privileges will be dropped after binding the port and the process will run as the user unbound.
The UID and GID for this user are 1000 by default, so be careful if you mount your unbound.conf, that it is readable for this user.
If you need to set a custom UID and GID, add the user key to your docker-compose.yml.
Example:
user: "your_UID:your_GID"
Complete docker-compose.yml with the `user` key
---
networks:
unbound:
name: unbound
driver: bridge
services:
# Unbound - a validating, recursive, and caching DNS resolver. DOCKERIZED!
# https://hub.docker.com/r/bleala/unbound
# https://github.com/Bleala/Unbound-DOCKERIZED
unbound:
image: bleala/unbound:latest
container_name: unbound
hostname: unbound
restart: always
user: "your_UID:your_GID"
security_opt:
- no-new-privileges:true
environment:
# Optional: set your timezone, for correct container and log time, default to Europe/Vienna
TZ: Europe/Vienna
# Optional: set different Unbound configuration file location, default to /etc/unbound/unbound.conf
UNBOUND_CONFIG: /etc/unbound/unbound.conf
# Optional: set diffenrent Unbound root.key file location, default to /var/unbound/root.key
UNBOUND_ROOT_FILE: /var/unbound/root.key
env_file:
- path: .env
required: false
networks:
unbound: {}
ports:
- target: 53
published: 53
protocol: tcp
mode: host
- target: 53
published: 53
protocol: udp
mode: host
volumes:
- type: bind
source: /path/to/unbound.conf
target: /etc/unbound/unbound.conf
read_only: trueYou can set three different environment variables if you want to:
| Variable | Info | Value |
|---|---|---|
TZ |
To set the correct container and log time | optional, default to Europe/Vienna, look here for possible values |
UNBOUND_CONFIG |
Set a custom path where unbound should look for an unbound.conf |
optional, default to /etc/unbound/unbound.conf |
UNBOUND_ROOT_FILE |
Set a custom path where unbound should look for the root.key |
optional, default to /var/unbound/root.key |
Clone this repo and then:
cd Unbound-DOCKERIZED/docker
docker build -t bleala/unbound:dev .Or you can use the provided docker-compose.override.yml file:
docker compose -f docker-compose.override.yml buildFor more information on using multiple compose files see here. You can also find a prebuilt docker image from Docker Hub, which can be pulled with this command:
docker pull bleala/unbound:latestI'm glad, if you want to contribute something to the Unbound container.
Feel free to create a PR with your changes and I will merge it, if it's ok.
Attention: Please use the main branch for pull requests, a CI pipeline is going to run to check, if the container will build!
1.24.0 - 18.10.2025:
- Enable Pie and Relro for Unbound
- Change healthcheck to root DNS
- Update Unbound to version 1.24.0
- Update Alpine to version 3.22.2
Current Versions:
- Unbound 1.24.0, Alpine 3.22.2
Old Version History
1.23.1 - 30.08.2025:
- Unbound Version: 1.23.1
- Alpine Version: 3.22.1
1.23.0 - 30.05.2025:
- Initial Release.
- Unbound Version: 1.23.0
- Alpine Version: 3.21.3