PiHole is a sinkholing, ad-blocking, self-hosted dns server, and Unbound is a self-hosted recursive DNS resolver. Together, they can completely negate your need for using any third-party DNS except for the root servers which keep the entire internet running.

However… DNS is always a problem.

It's always DNS

Rule #1 of network problems: It’s DNS. Always check DNS. I don’t care what you think, it’s DNS. Check it again. Restart the DNS containers. Check DNS settings again. Reboot the server that’s hosting it. Check it again. Okay, now you can start to blame something else.

Hosting in Docker

Hosting both PiHole and Unbound in docker gives you quite a lot of flexibility, but can be a headache to get working right. Here you go.

pihole:
  container_name: pihole
  image: pihole/pihole:latest
  restart: unless-stopped
  volumes:
    - /opt/docker/pihole/etc/pihole:/etc/pihole
    - /opt/docker/pihole/etc/dnsmasq:/etc/dnsmasq
  ports:
    # Don't remap these to different ports!
    - 53:53/tcp
    - 53:53/udp
 
    # This one can be remapped to your preference.  It's the web interface.
    - 8001:80/tcp
 
  environment:
    TZ: "America/Chicago"
    # Pro tip: don't make your password "password" (put it in a secrets file
    # too!)
    FTLCONF_webserver_api_password: "password"
 
    # Listen on all interfaces.  Typically ill-advised, but inside the
    # docker container, necessary.
    FTLCONF_dns_listeningMode: 'ALL'
 
    # Where the pihole should direct queries it's not blocking.  Separate
    # with semicolons and a newline (and adjust to your preference.  I'm
    # only using the unbound one, but showing another for an example).
    FTLCONF_dns_upstreams: |-
      unbound#53;
      9.9.9.9
 
    # Enable reading dnsmasq configs.  Needed for later reverse proxy
    # wildcard stuff.
    FTLCONF_misc_etc_dnsmasq_d: True
 
  cap_add:
    - SYS_NICE
 
  # If you get messages saying it's running out of memory, turn this up.
  shm_size: 640m
 
  # Don't start PiHole until Unbound is running, or PiHole gets real mad.
  depends_on:
    - unbound
 
unbound:
  container_name: unbound
  image: klutchell/unbound:latest
  restart: unless-stopped
  volumes:
    - /opt/docker/unbound/config:/etc/unbound/custom.conf.d
  # You don't need to expose these ports for PiHole.  I do it so I can
  # directly query unbound for debugging.
  ports:
    - 5335:53/tcp
    - 5335:53/udp

Next, set up the Unbound config file. Head to wherever you mapped the Unbound config volume to, and edit unbound.conf:

server:
    verbosity: 1
    interface: 0.0.0.0
    port: 53
    do-ip4: yes
    do-ip6: no
    do-udp: yes
    do-tcp: yes

    root-hints: /etc/unbound/custom.conf.d/root.hints

    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: no
    edns-buffer-size: 1472

    prefetch: yes
    num-threads: 1
    so-rcvbuf: 400k

    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10

Now, grab the root hints. In the same folder, run curl https://www.internic.net/domain/named.root | sudo tee root.hints . Boot it up, and you should have DNS!

PiHole gravity database

I frequently have issues with PiHole not starting if it can’t resolve DNS yet (and if it’s the DNS resolver, when it’s not starting up… nothing can resolve DNS). I don’t know why it’s designed this way, but if it has no gravity database, it won’t launch.

In this case, temporarily set the upstream resolver to something that does work (I like Quad9), launch it to generate the gravity file, then reset the settings to the way you want them and continue.

See also

Custom Wildcard Records

Setting a custom record in your DNS server is useful for loads of reasons, not least of which would be use of a reverse proxy (you, a friend’s network you’re tunneled to, etc). It’s pretty easy to do:

  1. Go to the PiHole web interface
  2. Go to Settings Local DNS records
  3. In the box labeled Local DNS records, add the domain you want to resolve and the IP you want to resolve it to.

Great!

Unless you want a wildcard record (say, for a reverse proxy with lots of serivces). That gets annoying really fast, and for some reason, PiHole does not allow you to enter wildcard records either via the web UI or via config.

However, you can trick it via custom regex rules!

  1. Go to the PiHole web interface
  2. Go to Domains
  3. Add a new regex filter rule, substituting your\.domain\.com with your domain and 1.2.3.4 with your reverse proxy server:
    \.your\.domain\.com$;reply=1.2.3.4
    
  4. Add the rule as a blocked domain (doesn’t work if you set it to allow)

Be careful not to specify that the \. is the first thing in the regex (i.e. with a ^ ). If you used a rule like (^|\.)your\.domain\.com, you could resolve the your.domain.com to the same reply, but typically this isn’t desired.

If you have a friend who maps vpn.friend.com to his external IP, but then also uses the same level of subdomain for his services (gitea.friend.com), then you’ll have to add two rules. It would be real nice to use a negative lookahead rule like ^(?!vpn).+\.friend.com$, but PiHole does not support the ?! negative lookahead operator, so instead add two rules:

  1. A blacklist of \.friend\.com$;reply=1.2.3.4
  2. A whitelist of ^vpn.friend.com$

See also

An alternate method

I used to do this with a custom dnsmasq configuration file rule, but this turned out to break VPN access inside the network as it not only resolved all subdomains but also the domain itself to whatever you gave. Notes remain below, but it’s probalby not a good idea in light of the regex rule method which does not have this problem.

  1. cd over to whever you put the /etc/dnsmasq.d volume from the PiHole container
  2. Create a new config in there. I’ll call mine homelab.conf, and add the following contents, substituting your.domain.com with what you’re using in the reverse proxy. Note – leading dot, but no leading *!
    • address=/.your.domain.com/192.168.30.142
  3. If you hadn’t done it earlier, make sure the FTLCONF_misc_etc_dnsmasq_d environment variable is set to True in the PiHole settings. Otherwise, this file (/ these files, if you have multiple) will be ignored.
  4. Restart the PiHole container (or, for good measure, reboot the whole host, your router, modem, cycle circuit breakers, etc)