Update 20th January 2025: Happy new year! I’ve recently revisited my setup over the Christmas holidays and have improved my docker compose file to use internal networks and not expose any unnecessary ports. Any additional comments amended to the original article will be in italics like this.
Also with thanks to Kadar Anwar who emailed me about a year ago with suggestions for improving this post. It’s taken me a long time to update this, though looking after a baby doesn’t give me much free time nowadays!
The humble Raspberry Pi is a very versatile thing. A low-cost computer that can become a simple low-end desktop, a low power server or a controller for electronics projects via its numerous GPIO pins. In my case it’s the middle option, I currently have two Raspberry Pis managing various functions on my home network such as:
- DHCP to assign IP addresses and routing information to devices depending on what their purpose is.
- CUPS to allow my USB inkjet printer to receive print jobs from any computer on the network.
- Pi-Hole to block annoying (and potentially malicious) advertisements at network level
- Cloudflared (a.k.a. Argo Tunnel) to provide a channel for making DNS requests securely over HTTPS.
It’s the latter two that I’ll be focusing on in this post. For the most part this post is based on an existing how-to by Ben Dews, however recently I have been moving services into Docker containers for the purpose of quick disaster recovery (e.g. should the internal SD card fail). In the case of a full rebuild, just install Raspberry Pi OS, install Docker, run the docker-compose
script and everything should be back to normal quickly.
Prerequisites
From a fresh install of Raspberry Pi OS (formerly Raspbian), install Docker and docker-compose
from the package manager:
$ sudo apt update
$ sudo apt install docker.io docker-compose
Once those have been installed along with their dependencies, we can make a start with creating our docker-compose
script.
Creating the Stack
Since this stack will consist of two containers communicating with one-another, it’s better to make use of docker-compose
to organise the stack under one roof rather than bringing up and taking down two separate containers.
Creating the network
In order to make use of internal communications between the cloudflared
and pihole
containers, we need to first create the network that the two will be using. This is an optional step as any single docker compose file will have its own network automatically created, but I like to keep things nicely defined.
So in a terminal, firstly create the network:
# docker network create pihole_doh
This will create an external network (i.e. can be used by other docker compose configurations if you so choose) called pihole_doh
cloudflared
– DNS over HTTPS
So let’s create pihole-doh.yml
and firstly define our cloudflared
service along with the previously defined network:
network:
pihole_doh:
external: true
services:
cloudflared:
image: crazymax/cloudflared:latest
container_name: cloudflared
networks:
- pihole_doh
environment:
TZ: "Europe/London"
TUNNEL_DNS_UPSTREAM: "https://1.1.1.1/dns-query,https://1.0.0.1/dns-query"
restart: always
For those unfamiliar with docker-compose
(and I will readily admit I’m still a newcomer to this), this seems like a lot but I’ll break it down.
cloudflared:
image: crazymax/cloudflared:latest
container_name: cloudflared
Firstly we have the image this container will run, in this case I’m using an image of cloudflared
created by GitHub user crazy-max. There are a number of alternative images available so if you want to try a different implementation then other options are available. This particular image is the second-most popular image for cloudflared
on Docker Hub, with the other created by visibilityspots. The container_name
value is just a frendly name we assign to the container, else Docker will randomly generate one.
network:
- pihole_doh
This tells the cloudflared
container to use the pihole_doh
docker network we defined earlier.
environment:
- "TZ=Europe/London"
- "TUNNEL_DNS_UPSTREAM=https://1.1.1.1/dns-query,https://1.0.0.1/dns-query"
The environment section is for your own personal preferences. I live in the UK so I set the timezone accordingly, while the TUNNEL_DNS_UPSTREAM
parameter allows you to set your DNS-over-HTTPS provider of choice if you don’t want to use Cloudflare.
restart: always
Finally, we always want the container to restart in the event of an error, or if the Raspberry Pi reboots.
Verifying cloudflared
We can manually verify the cloudflared
service is working by deploying the container and making a DNS request using dig
. However firstly we need to temporarily expose port 5053 to the wider network by adding:
ports:
- "5053:5053/udp"
under the cloudflared
service in docker-compose.yml
$ docker-compose -f "pihole-doh.yml" up -d
Once the container has successfully started we can make a DNS query over port 5053 using dig
:
$ dig @127.0.0.1 -p 5053 michaeldodd.net
; <<>> DiG 9.11.5-P4-5.1+deb10u2-Debian <<>> @127.0.0.1 -p 5053 michaeldodd.net
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16039
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: 27b0cfa3930c9191 (echoed)
;; QUESTION SECTION:
;michaeldodd.net. IN A
;; ANSWER SECTION:
michaeldodd.net. 883 IN A 69.163.157.115
;; Query time: 3 msec
;; SERVER: 127.0.0.1#5053(127.0.0.1)
;; WHEN: Sun Nov 08 12:41:37 GMT 2020
;; MSG SIZE rcvd: 87
We can verify that the cloudflared
container is making this request by using:
$ docker-compose -f "pihole-doh.yml" down
to bring down the container and re-running the dig
command. This time it should time out.
$ dig @127.0.0.1 -p 5053 michaeldodd.net
; <<>> DiG 9.11.5-P4-5.1+deb10u2-Debian <<>> @127.0.0.1 -p 5053 michaeldodd.net
; (1 server found)
;; global options: +cmd
;; connection timed out; no servers could be reached
Remember to remove the ports:
definition again once you’re done verifying.
Pi-Hole – Network-wide ad-blocking
Time to add our second service:
pihole:
container_name: pihole
image: pihole/pihole:latest
networks:
- pihole_doh
depends_on:
- cloudflared
links:
- cloudflared
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp"
- "443:443/tcp"
environment:
TZ: 'Europe/London'
WEBPASSWORD: 'superSecurePasswordHonest!'
DNS1: 'cloudflared#5053'
DNS2: 'no'
# Volumes store your data between container upgrades
volumes:
- './pihole/etc-pihole/:/etc/pihole/'
- './pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/'
restart: always
As above, we give our container a friendly name, and this time we’re using an official image provided by Pi-Hole. We want to ensure that this service comes up after cloudflared
is ready, and that we can communicate with the cloudflared
container. We also want docker to establish a network link between cloudflared
and pihole
using the pihole_doh
network.
network:
- pihole_doh
depends_on:
- cloudflared
links:
- cloudflared
Next we have our list of ports:
ports:
- "53:53/tcp"
- "53:53/udp"
- "67:67/udp"
- "80:80/tcp"
- "443:443/tcp"
A few more this time, and most of these we want to directly map:
- 53 (TCP and UDP) – DNS
Listening ports for DNS requests. It’s important to directly map these ports as DNS handling is at the heart of what Pi-Hole does. - 67 – DHCP
Pi-Hole can also act as a DHCP server so it can be beneficial to leave this in. However in my personal setup I have removed this mapping as I am usingisc-dhcp-server
to handle DHCP requests on my home network. - 80 – Web Server
The web interface for Pi-Hole’s admin console. This one should be safe to map to whatever port you like, for example to avoid conflict if you’re also running a web server. - 443 – SSL
Allows Pi-Hole to catch adverts served up over SSL.
There are a number of environment variable configuration options available on the Docker Hub page, but for now we’ll only make use of a few.
environment:
TZ: 'Europe/London'
WEBPASSWORD: 'superSecurePasswordHonest!'
DNS1: 'cloudflared#5053'
DNS2: 'no'
The TZ
value is the same as before albeit formatted differently due to the way the image is set up. This could probably be improved upon by using variable substitution so we only need to change one line to change the timezone for all services.
It’s generally a very bad idea to stick any credentials within a docker-compose
file, and docker-compose
allows for the importing of secrets. But for the sake of simplicity we’ll use a plain ol’ password here.
Finally, we want to configure Pi-Hole to make use of secure DNS requests by ensuring that upstream DNS requests are only routed via our cloudflared
service. Therefore we’re sending all upstream DNS queries via the docker container network on port 5053, and not using any additional DNS providers. Having our own internal pihole_doh
network allows us to just specify the name of the cloudflared
service as the address, and docker will automatically resolve it for our pihole
container.
# Volumes store your data between container upgrades
volumes:
- './pihole/etc-pihole/:/etc/pihole/'
- './pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/'
These lines create storage directories for Pi-Hole outside of the container, so that configuratons are retained if the container is recreated after an upgrade. These folders will be created in the same directory as the pihole-doh.yml
config file.
# Recommended but not required (DHCP needs NET_ADMIN)
# https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
cap_add:
- NET_ADMIN
- NET_BIND_SERVICE
restart: always
Finally, some additional capabilities that Pi-Hole requires should you wish to use it as a DHCP server. Additional details can be found here.
Running and verifying the stack
So your final pihole-doh.yml
file should look something like this:
network:
pihole_doh:
external: true
services:
cloudflared:
image: crazymax/cloudflared:latest
container_name: cloudflared
networks:
- pihole_doh
environment:
TZ: "Europe/London"
TUNNEL_DNS_UPSTREAM: "https://1.1.1.1/dns-query,https://1.0.0.1/dns-query"
restart: always
pihole:
container_name: pihole
image: pihole/pihole:latest
networks:
- pihole_doh
depends_on:
- cloudflared
links:
- cloudflared
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp"
- "443:443/tcp"
environment:
TZ: 'Europe/London'
WEBPASSWORD: 'superSecurePasswordHonest!'
DNS1: 'cloudflared#5053'
DNS2: 'no'
# Volumes store your data between container upgrades
volumes:
- './pihole/etc-pihole/:/etc/pihole/'
- './pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/'
restart: always
(It should go without saying that you should use a different password, or better still, put your credentials in a different file)
The next thing to do is to run the script from docker-compose
:
$ docker-compose -f pihole-doh.yml up -d
Providing you don’t have any other services that have already nabbed the ports, both containers should be up and running within a few seconds. You can verfiy this by visiting the IP of your Raspberry Pi in a web browser (e.g. http://192.168.0.10/admin
), which should present you with the Pi-Hole admin panel.
You can now also verify that your DNS requests are being made over HTTPS by visiting Cloudflare’s ESNI Checker tool. After running the test, the first two columns (Secure DNS and DNSSEC) should both be green. The latter two (TLS 1.3 and Encrypted SNI) are browser-based features so fall outside the scope of this post.
The last thing to do is to ensure that all devices in your network are using your Raspberry Pi’s IP address as its DNS server. This can be done via the DHCP settings on your router or DHCP server, or manually on each device.
… or you can run this locally
During the process of verifying these steps on a Debian virtual machine, I found that I could run this stack locally, configuring the DNS server on my network connection to point to 127.0.0.1
. This means that when used in combination with a VPN this stack could provide an extra layer of security when using public WiFi networks for example, as well as blocking annoying adverts without the need for a browser extension.
While I’ve locally tested this under Debian linux, I’ve not been able to verify this as working under Mac OS or Windows. I will update this post with my findings as and when I’m able to check.
UPDATE February 2022 – I’ve revisited this, and by setting the value of DNS1
to point at the Docker container network (172.22.0.1
) rather than localhost, I now have DNS-over-HTTPS running locally on my MacBook. I have updated this post to reflect this.
Leave a Reply
You must be logged in to post a comment.