a few months ago I configured a thin client as my home server to replace the previous raspberry pi setup.

During that migration I moved over all native services within docker containers. One of those services being a pi-hole setup to block ad serving domains on dns level and to have a dns cache within our LAN to gain a bit of speed.

It has been running ever since without any issue and worked pretty well.

When cloudflare announced their fast and privacy based DNS resolver I got a bit intrigued by their DNS over HTTPS feature. Especially since our ISP telenet is using our web history for their advertisements too.

So I stumbled on some articles from Oliver Hough and Scott Helme that describe how you can combine a cloudflared proxy-dns with pi-hole to get your dns requests encrypted through HTTPS and still be able to filter out the advertisements.

Since I got everything in docker I configured a cloudflared and a pi-hole container both automated through travis (cloudflared & pi-hole) with dgoss tests.

I got some inspiration from maartje who used a matrix to build multiple docker images for different architectures using travis. The main reason behind this was that after I got this setup up and running using this docker-compose file on my x86_64 machine:

$ cat docker-compose.yml
version: "3"

    container_name: cloudflared
    image: visibilityspots/cloudflared:amd64
    restart: unless-stopped

    container_name: pi-hole
    image: visibilityspots/pi-hole:amd64
    restart: unless-stopped
      - "80:80/tcp"
      - "53:53/tcp"
      - "53:53/udp"
      - ServerIP=
      - IPv6=false
      - TZ=CEST-2
      - WEBPASSWORD=admin
      - CloudflaredServer=

    driver: bridge
       - subnet:

I remembered this project where a raspberry pi zero W was used together with a tiny display. In the meanwhile I have the DoH cloudflared/pi-hole combination running on such a tiny device using ArchLinux ARM and ordered the display :D

You can use the same dockerfile on a raspberry pi zero but with other tags for the container images:

image: visibilityspots/cloudflared:arm
image: visibilityspots/pi-hole:armel

As you can see unfortunately I had to configure static ip's since the dnsmasq config needs the ip address of the cloudflared service. If someone has a better solution to implement it let me know!

I also opted to not store the data. Meaning that when the docker containers are restarted the data is gone.

So when you now bring up those 2 containers:

$ docker-compose up -d
Creating network "###_pihole_net" with driver "bridge"
Creating pi-hole ...
Creating cloudflared ...
Creating pi-hole
Creating cloudflared ... done
$ docker-compose logs cloudflared
Attaching to cloudflared
cloudflared    | time="2018-04-16T20:01:14Z" level=info msg="Adding DNS upstream" url=""
cloudflared    | time="2018-04-16T20:01:14Z" level=info msg="Adding DNS upstream" url=""
cloudflared    | time="2018-04-16T20:01:14Z" level=info msg="Starting DNS over HTTPS proxy server" addr="dns://"
cloudflared    | time="2018-04-16T20:01:14Z" level=info msg="Starting metrics server" addr=""
$ docker-compose logs pi-hole
Attaching to pi-hole
pi-hole        | [services.d] starting services
pi-hole        | Starting lighttpd
pi-hole        | Starting dnsmasq
pi-hole        | Starting crond
pi-hole        | Starting pihole-FTL (no-daemon)
pi-hole        | [services.d] done.
pi-hole        | dnsmasq: started, version 2.76 cachesize 10000
pi-hole        | dnsmasq: compile time options: IPv6 GNU-getopt DBus i18n IDN DHCP DHCPv6 no-Lua TFTP conntrack ipset auth DNSSEC loop-detect inotify
pi-hole        | dnsmasq: using nameserver
pi-hole        | dnsmasq: read /etc/hosts - 7 addresses
pi-hole        | dnsmasq: read /etc/pihole/local.list - 2 addresses
pi-hole        | dnsmasq: failed to load names from /etc/pihole/black.list: No such file or directory
pi-hole        | dnsmasq: read /etc/pihole/gravity.list - 121065 addresses
pi-hole        | dnsmasq: 1 query[A] pi.hole from
pi-hole        | dnsmasq: 1 /etc/pihole/local.list pi.hole is

you should be able to query the containerized pi-hole DNS service from it's host or from within your netwerk using dig:

$ dig @localhost -p 53 visibilityspots.org
($ dig @IP-ADDRESS-OF-DOCKER-NODE -p 53 visibilityspots.org)

; <<>> DiG 9.12.1 <<>> @localhost -p 53 visibilityspots.org
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51155
;; flags: qr rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 0, ADDITIONAL: 1

; EDNS: version: 0, flags:; udp: 1536
;visibilityspots.org.       IN  A

visibilityspots.org.    37  IN  A
visibilityspots.org.    37  IN  A
visibilityspots.org.    37  IN  A
visibilityspots.org.    37  IN  A
visibilityspots.org.    37  IN  A
visibilityspots.org.    37  IN  A
visibilityspots.org.    37  IN  A
visibilityspots.org.    37  IN  A

;; Query time: 223 msec
;; SERVER: ::1#53(::1)
;; WHEN: Mon Apr 16 22:05:37 CEST 2018
;; MSG SIZE  rcvd: 328

Obviously I wanted to see myself that when sniffing the network the DNS requests aren't readable so I used tcp dump to prove myself the data was sent through HTTPS

pi-hole# tcpdump -i eth0 udp port 53
22:39:30.837594 IP > piholeContainerID.domain: 36972+ [1au] A? visibilityspots.com. (60)
22:39:31.009345 IP piholeContainerID.domain > 36972 8/0/1 A, A, A, A, A, A, A, A (328)
cloudflared# tcpdump -i eth0 udp port 54
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
20:39:29.029132 IP piholeContainerID.59189 > cloudflaredContainerID.54: UDP, length 40
20:39:29.069864 IP cloudflaredContainerID.54 > piholeContainerID.59189: UDP, length 116
20:39:30.838803 IP piholeContainerID.28892 > cloudflaredContainerID.54: UDP, length 60
20:39:31.003756 IP cloudflaredContainerID.54 > piholeContainerID.28892: UDP, length 328
20:39:31.352487 IP piholeContainerID.50291 > cloudflaredContainerID.54: UDP, length 31
20:39:31.364073 IP piholeContainerID.16365 > cloudflaredContainerID.54: UDP, length 31
20:39:31.411227 IP cloudflaredContainerID.54 > piholeContainerID.50291: UDP, length 156
20:39:31.432364 IP cloudflaredContainerID.54 > piholeContainerID.16365: UDP, length 218

So by now you can configure this new DNS service on your router or dhcp daemon within your local network.

Since the pi isn't running for a very long time I have no clue if it can cope with the load on our network but I'll keep you posted ;)