You are here because you want to run Pi-Hole and Unbound locally on your own laptop so that you have a portable ‘as secure as can be’ internet browsing experience. This eliminates the need to carry around an external raspberry pi / pi-hole device.
I’m going to outline a solution running on an Ubuntu/Mint linux install and we will,
- Carry out a native install of Unbound on your laptop.
- Install Pi-Hole as a docker container.
Have tried a few online docker containers of Unbound but they all failed for me, if this changes I will update this guide to run fully on Docker.
Here are the software versions I used in this guide.
- Unbound Version 1.13.1
- Pihole Container Version 2023.01.10
- Linux Mint 21.1 “Vera”
- Docker version 20.10.23
- Docker Desktop 4.16.2
Please note that you can only run one virtualisation technology on your laptop at one time, so if you are running VirtualBox you are going to have to shut it down while using any docker containers. The reverse is true too… if you try and start a virtual machine while running docker you are going to have a bad time.
DNS Leak Protection
As briefly mentioned in one of my previous guides, https://rexbytes.com/2022/09/19/pi-hole-a-portable-docker-laptop-solution/ ,
benefiting from the protection that your Pi-hole give you can and does in most cases expose you to DNS leaks.
When running your VPN and Pi-hole setup go ahead and visit https://dnscheck.tools/ or https://www.dnsleaktest.com/ .
You will find that the DNS provider(s) detected are the ones chosen by you in the Pi-hole admin page, not the DNS server provided by your VPN connection. This isn’t really a fault of the pi-hole project, it’s just that pi-hole isn’t designed to also handle your DNS resolving so it asks for an upstream DNS server instead.
The solution is to be your own DNS resolver and install Unbound.
DNS isn’t a perfect technology and just using it someone, something, somewhere will know which websites you are visiting – this is by design.
You can however limit your exposure and maximize your protection, so why bother installing Unbound?
- Google, Cloudflare and DNS resolving services as a whole may or may not be creating a profile on your web browsing history to sell to marketing companies. Why not just opt out?
- These same DNS resolver providers are highly centralised and are a very tempting target for hackers, if in the unlikely event hacker access these central DNS records they may gain enough information to target you.
- In combination with a VPN tunnel you immediately protect yourself in public wifi connections, cafes, airports, and even unsecured home wifi connections at elderly relative homes. Your DNS request become invisible to the local network.
- Most VPN providers claim to not log DNS queries.
You might stumble upon the topic of DoH, which is DNS over HTTPS, although this would encrypt your DNS requests you could still be at the mercy of an upstream provider as they would still be able to create an advertising profile on your behaviour. Cloudflare for example allows you to connect using DoH requests.
I personally consider Pi-hole, with Unbound over a VPN provided by a VPN provide based in Switzerland the best choice… it is in my opinion better than running DoH. You could run DoH through your VPN buy that would defeat the purpose of a VPN, if you run DOH without a VPN you are still open to threats on local networks because they can see the ip addresses from traffic… https or not.
If you go through all the above… only to then sign in to facebook and other FANG services… well, they will build a profile of your web activity anyway. Not saying it would be a waste of time setting up all the DNS security, but be aware that a secure connection is only as secure as the data you share with the services you think you can trust.
Order of DNS requests
Once fully setup your DNS request route will look like this.
Web browser (or any other web client) > [port:53] Local Docker Pi-Hole >[port:5335] Local Native Unbound Resolver > [automatic] VPN DNS resolver
For further benefits of using your own Unbound DNS resolver consult their documentation.
Install & Configure Unbound
You know how your pi-hole asks if you want google, cloudflare or any other upstream DNS resolver set as your DNS provider?

Well, why not become your own? This in short is what Unbound enables you to do. For a complete description of the Unbound project please read more at the following location. https://www.nlnetlabs.nl/projects/unbound/about/
https://unbound.docs.nlnetlabs.nl/en/latest/getting-started/installation.html
I am on linux mint, so the install is as easy as,
sudo apt update
sudo apt install unbound
This will install Unbound on your system. We will configure Unbound to play nice with your docker pi-hole.
Setting up Unbound
Let’s setup Unbound.
unbound.conf
This is the bare minimum unbound.conf file.
Pi-hole will already be listening for all local DNS requests on port 53 so in this config we will need to set Unbound to listen for requests inbound from the pi-hole on another port, we will use port 5335.
We want to accept connections from our local loopback address, in the case where we don’t want to use a VPN and so we set an interface to 127.0.0.1@5335 . Mainly this allows you to run a dig command to check the state of Unbound from a local native terminal. More on dig later.
We also want to listen to requests from the Docker default bridge network, which for my non-root user is 172.17.0.1@5335
Download a copy of the latest root hints file, https://www.iana.org/domains/root/files , and save it at the following file on your host system, /var/lib/unbound/root.hints You are going to be sending DNS queries direct to the source.
Copy the following text to ‘/etc/unbound/unbound.conf‘ , backup the origional unbound.conf that came with the fresh install if you wish.
server:
# If no logfile is specified, syslog is used
# logfile: "/var/log/unbound/unbound.log"
verbosity: 1
interface: 127.0.0.1@5335
interface: 172.17.0.1@5335
do-ip4: yes
do-udp: yes
do-tcp: yes
# May be set to yes if you have IPv6 connectivity
do-ip6: no
# You want to leave this to no unless you have *native* IPv6. With 6to4 and
# Terredo tunnels your web browser should favor IPv4 for the same reasons
prefer-ip6: no
# Use this only when you downloaded the list of primary root servers!
# If you use the default dns-root-data package, unbound will find it automatically
root-hints: "/var/lib/unbound/root.hints"
# Trust glue only if it is within the server's authority
harden-glue: yes
# Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
harden-dnssec-stripped: yes
# Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
# see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
use-caps-for-id: no
# Reduce EDNS reassembly buffer size.
# IP fragmentation is unreliable on the Internet today, and can cause
# transmission failures when large DNS messages are sent via UDP. Even
# when fragmentation does work, it may not be secure; it is theoretically
# possible to spoof parts of a fragmented DNS message, without easy
# detection at the receiving end. Recently, there was an excellent study
# >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<<
# by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
# in collaboration with NLnet Labs explored DNS using real world data from the
# the RIPE Atlas probes and the researchers suggested different values for
# IPv4 and IPv6 and in different scenarios. They advise that servers should
# be configured to limit DNS messages sent over UDP to a size that will not
# trigger fragmentation on typical network links. DNS servers can switch
# from UDP to TCP when a DNS response is too big to fit in this limited
# buffer size. This value has also been suggested in DNS Flag Day 2020.
edns-buffer-size: 1232
# Perform prefetching of close to expired message cache entries
# This only applies to domains that have been frequently queried
prefetch: yes
# One thread should be sufficient, can be increased on beefy machines. In reality for most users running on small networks or on a single machine, it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
num-threads: 1
# Ensure kernel buffer is large enough to not lose messages in traffic spikes
# so-rcvbuf: 1m
# Ensure privacy of local IP ranges
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
#This is the docker root default bridge network.
private-address: 172.16.0.0/12
#This is my non-root docker user bridge network.
private-address: 172.17.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
remote-control:
#Yea, this says "remote" , but setting the interface ip address
#to local loopback enables you to check the Unbound cache and
#run other checks.
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
systemctl systemd unbound.service
I’m going to quote an error you will get if you don’t setup unbound.service to wait for docker to start,
error: can’t bind socket: Cannot assign requested address for 172.17.0.1 port 5335
I hope people searching online for help with this common issue will find their way here.
In short, Unbound is looking for a network that doesn’t exist – yet.
Since we are running pihole in a docker container we need to make sure docker desktop is always up and running before your local install of unbound otherwise unbound will not be able to bind to 172.17.0.0 , this means unbound will ALWAYS fail on startup and you will have to manually start it by using ‘systemctl start unbound‘ , to avoid this problem we are going to edit the unbound service file.
As root, open ‘/lib/systemd/system/unbound.service‘ and add ‘After=docker.service‘ in the same place I have.
[Unit]
Description=Unbound DNS server
Documentation=man:unbound(8)
After=network.target
After=docker.service
Before=nss-lookup.target
Wants=nss-lookup.target
[Service]
Type=notify
Restart=on-failure
EnvironmentFile=-/etc/default/unbound
ExecStartPre=-/usr/lib/unbound/package-helper chroot_setup
ExecStartPre=-/usr/lib/unbound/package-helper root_trust_anchor_update
ExecStart=/usr/sbin/unbound -d -p $DAEMON_OPTS
ExecStopPost=-/usr/lib/unbound/package-helper chroot_teardown
ExecReload=+/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target
Unbound will now patiently wait in line for docker to start, if docker fails to start, unbound will probably attempt to start anyway as systemd After and Before ordering is only a suggestion. Keep this in mind when troubleshooting.
Configure Pi-hole
I am assuming you have followed the linux instructions on setting up pihole on your laptop, which can be found here…
Make sure you also pay attention to the port53 conflict in the above linked guide.
We need to modify the docker-compose.yml file to enable our pi-hole ( which is running in a container, so is in an internal docker default bridge network) to make requests to your host computer which is running Unbound natively.
To allow communication between the docker bridge network and services on your host we use a ‘magic ip-address’, this ip address is activated by adding the extra_hosts option with the value ‘host.docker.internal:host-gateway‘. The ip address 192.168.65.2 can now be referenced by any container in mentioned in your docker-compose.yml file to contact your host. This ip-address is internal to docker and is a constant.
version: "3"
services:
pihole:
container_name: pihole
image: pihole/pihole:2023.01.10
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp"
environment:
TZ: 'America/Chicago'
WEBPASSWORD: 'choose a password'
WEB_UID: 999
PIHOLE_DNS_: 192.168.65.2#5335
DNSSEC: 'true'
DNSMASQ_LISTENING: local
volumes:
- './etc-pihole:/etc/pihole'
- './etc-dnsmasq.d:/etc/dnsmasq.d'
restart: unless-stopped
Notice the pihole environment variables we set.
WEB_UID: 999 is the docker container workaround we mentioned in the setup guide to enable writting to the pihole database, for when you want to add addlists and black and white lists.
PIHOLE_DNS_: takes advantage of dockers magic ip address and routes all your DNS requests to port 5335 on your local host.
This enables your dockerized pihole to talk with your host install of Unbound.
Graphically inside the pihole admin page it will look like this,

Notice, when you check your stats inside pi-hole you will see ‘host.docker.internal#5335‘ but if you try using that string as a custom DNS server in the above settings section pihole will reject it… strange. Hmmm. Stick with ‘192.168.65.2#5335‘

DNSMASQ_LISTENING: local, this will only allow local DNS requests – requests from your laptop.

DNSSEC: ‘true’, will enable validation of DNS replies and cache DNSSEC data.

Network Settings
I set my wifi and vpn networks DNS to the local server ip address, the following images look the same… but one is a screen shot of my wifi settings, the other from my VPN. Both direct DNS queries to the containerized pi-hole listening on the default DNS port 53.


Dig for DNS
Start up your docker container and lets do some dig queries,
DNS query from the host computer
ubuntu@goodboy:~$ dig google.com @127.0.0.1 -p 5335
; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> google.com @127.0.0.1 -p 5335
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5766
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 300 IN A 172.217.169.46
;; Query time: 463 msec
;; SERVER: 127.0.0.1#5335(127.0.0.1) (UDP)
;; WHEN: Sun Jan 29 16:31:34 CET 2023
;; MSG SIZE rcvd: 55
DNS query from inside the pi-hole
Enter your pihole container,
ubuntu@goodboy:~$ docker container exec -it pihole bash
Remember we are going to have to use the magic ip address to route our dig query to host local loopback.
root@38843fae6f36:/# dig google @192.168.65.2 -p 5335
; <<>> DiG 9.16.33-Debian <<>> google @192.168.65.2 -p 5335
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48392
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google. IN A
;; AUTHORITY SECTION:
google. 900 IN SOA ns-tld1.charlestonroadregistry.com. cloud-dns-hostmaster.google.com. 1 21600 3600 259200 900
;; Query time: 152 msec
;; SERVER: 192.168.65.2#5335(192.168.65.2)
;; WHEN: Sun Jan 29 09:44:28 CST 2023
;; MSG SIZE rcvd: 133
DNS leak test
Try this yourself, visit,
Unbound Issues
Problem: Unbound stopped resolving DNS queries intermittently
I have noticed that if you leave your computer on for a long period Unbound stops resolving, which also causes secondary issues such as a VPN connection being terminated… unusual.
I’ve not found a solid fix except for the following.
Restart your Unbound using,
ubuntu@goodboy:~$ sudo systemctl restart unbound
this will make sure unbound resumes resolving DNS queries.
This though is inconvenient if you are using your machine to provide this service to a household. Instead add it to your root crontab, my choice is to run this restart command every half hour.
root@goodboy:~# crontab -e
Then add the following line.
*/30 * * * * /usr/bin/systemctl restart unbound
Here is what my full crontab looks like,
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
# Unbound seems to stop resolving after some time, so let's restart the service every 30 mins.
*/30 * * * * /usr/bin/systemctl restart unbound
This solution works for me, Unbound has been serving DNS queries successfully for a long period of time.
If you know of any other way other than my workaround please let me know in the comments.
Thanks.
VPN KillSwitch Software
If you are using the following software I have made available,
this setup can still be used with the VPN killswitch option as it is tun+ interface based.
If you want the –protect setting without using a vpn I have created the –punboundpi switch, which will run your laptop in protect mode and take in to account unbound running on port 5335 and pi running locally on port 53.
For example, if you are connecting to a VPN router you may just want normal related/established network protection.
Enjoy!
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-N RB_I_RELATED_AND_ESTABLISHED
-N RB_O_RELATED_AND_ESTABLISHED
-A INPUT -j RB_I_RELATED_AND_ESTABLISHED
-A INPUT -m state --state INVALID -j DROP
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A FORWARD -m state --state INVALID -j DROP
-A OUTPUT -j RB_O_RELATED_AND_ESTABLISHED
-A OUTPUT -m state --state INVALID -j DROP
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
-A RB_I_RELATED_AND_ESTABLISHED -p udp -m udp --dport 5335 -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
-A RB_I_RELATED_AND_ESTABLISHED -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A RB_I_RELATED_AND_ESTABLISHED -i lo -p udp -m udp --dport 53 -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
-A RB_I_RELATED_AND_ESTABLISHED -p tcp -m conntrack --ctstate NEW,ESTABLISHED -m multiport --dports 22,80,443 -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -p udp -m udp --sport 5335 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -i lo -p udp -m udp --sport 53 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -p tcp -m conntrack --ctstate NEW,RELATED,ESTABLISHED -m multiport --sports 22,80,443 -j ACCEPT
[…] https://rexbytes.com/2023/01/29/portable-laptop-pi-hole-and-unbound-dns-resolver/#unboundresolving […]
[…] unit file to reference from another service unit file… for example, if you follow the unbound tutorial you need to wait for docker to be up and running with a pihole container before starting the unbound […]