A few weeks ago, I wrote about implementing a DNS blocklist script for Synology servers. It was fun to write and refresh my bash shell script skills, but it quickly became apparent that the Synology DNS server was just not up to the huge number of DNS sinkholes I was trying to throw at it.
I had steered away from using another method for some time, mainly because I use Synology Directory Server for PC authentication and that relies on the DNS Server. I am not keen on replicating systems, unless an existing system cannot do what I want it to, and generally replace the old with the new.
In this case, however, I need to keep the Synology DNS server, if only to support the Directory Server. As a result, I decided to deploy a Pi-hole server, which (unsurprisingly) originated as a DNS sinkhole server, allowing you to block unwanted DNS requests, on a Raspberry Pi. Because of its origins, Pi-hole has a small footprint too, so is ideal.
Docker and Networks
Running up a Docker instance of Pi-hole is simple enough, but as it also uses port 53, as does the Synology DNS server, I couldn’t run it on a Docker bridge or on the host (which is not best practice in any case). That meant implementing a MACVLAN network, allowing the Docker instance to appear on the host network with a different IP address.
DNS over HTTPS
At the same time, I decided that, rather than use standard DNS, I would implement DNS over HTTPS where any request made outside my network would be made securely without logging or tracking.
Pi-hole on its own doesn’t support DNS over HTTPS (DoH), so I needed another solution. Enter Cloudflared, a DNS proxy server that listens for requests on port 53, but does its own requests over 443.
Running the Docker Compose File
If you just want to go ahead and run the Docker Compose file (using the command line, or something handy like Portainer) then you can copy the full script below. The script itself uses environment variables which you can modify and store either in an .env
file along with the docker_compose.yaml
file.
Alternatively you can import the .env
file into Portainer, or add the variables manually.
Note that there are still some sections of the Docker Compose file that you need to update, to make sure that the MACVLAN matches your existing network and doesn’t conflict with existing IP addresses.
Environment Settings
For both the Pi-hole and Cloudflared environment settings in the compose script, I have substituted some values with variables that you can set separately in the .env
file.
The TZ
is a standard Linux timezone (e.g. “Europe/London”).
The ServerIP
variable is the IP address of Pi-Hole (In my case, I used 192.168.1.8)
The PIHOLE_DNS_
variable references the Cloudflared IP address (I used 192.168.1.9).
It’s fairly important to use adjacent IP addresses, because we are going to set the MACVLAN up to only be available to IP addresses using CIDR notation.
And finally, the VIRTUAL_HOST
is a domain name for the Pi-hole server. You don’t need to reference it or register it anywhere, but it needs to exist for Pi-hole.
For the Cloudflared environment settings, I have set both DNS resolvers to the Cloudflare servers, because they are extremely responsive, support DNSSEC, and don’t log requests. In fact, these variables are default within the Dockerfile itself, so they aren’t necessary, but I have included them for completeness. You can change these to any number of other servers, a list of which can be found at https://dnscrypt.info/public-servers/.
Pi-Hole Environment Settings
TZ: ${TIMEZONE}
ServerIP: ${PIHOLE_IP}
PIHOLE_DNS_: ${CLOUDFLARED_IP}#53'
VIRTUAL_HOST: ${HOST}.${DOMAIN_NAME}
Cloudflared Environment Settings
TZ: ${TIMEZONE}
PORT: 53
ADDRESS: 0.0.0.0
UPSTREAM1: https://1.1.1.1/dns-query
UPSTREAM2: https://1.0.0.1/dns-query
The depends_on Parameter
The depends_on
parameter tells Pi-Hole to wait until Cloudflared is running and is reporting itself as healthy. For Docker Compose version 3 scripts, the condition
value was deprecated, but for the latest version of Docker Compose itself (1.29 and above), you can omit the version from the script and use condition
again.
- '/docker/appdata/pihole/:/etc/pihole/'
- '/docker/appdata/pihole/dnsmasq.d/:/etc/dnsmasq.d/'
services:
cloudflared:
condition: service_healthy
Pi-hole Volumes
The Pi-hole volumes map two directories needed to store the Pi-hole data and the dnsmasq data.
Pi-hole Network Setup
There are a number of values needed to be set up for Pi-hole. The hostname
is preprended to the domainname
(which are set in the .env
file).
The networks
section references the MACVLAN created at the end of the Docker Compose script (dnsnet
), with the IP address of the Pi-hole server.
The dns
section references the localhost (i.e. Pi-hole), and if needed, calls out to 1.1.1.1 (The Cloudflare DNS server hosted on port 53), which shouldn’t be necessary.
The cap_add
section is only needed if you intend to use Pi-Hole as your DHCP server as well. I have because, unless your gateway (and DHCP server, which typically is your router) can resolve IP addresses to names, Pi-hole will just present IP addresses in your statistics, which can get confusing.
hostname: ${HOST}
domainname: ${DOMAIN_NAME}
mac_address: ec:32:45:4d:01:a1
networks:
dnsnet:
ipv4_address: ${PIHOLE_IP}
cap_add:
- NET_ADMIN
dns:
- 127.0.0.1
- 1.1.1.1
Cloudflared Network Setup
The Cloudflared network setup is a lot more simple than for Pi-hole: it simply needs the ipv4_address
of the Cloudflared instance which is supplied in the .env
file.
networks:
dnsnet:
ipv4_address: ${CLOUDFLARED_IP}
The MACVLAN Network
Finally, we create the MACVLAN network (yes, I know that is RAS syndrome ) to allow Pi-Hole and Cloudflared to use IP addresses in the same subnet as the Docker host, and still use port 53 for interrogation.
The parent for the driver_opts
section refers to the physical ethernet port that the MACVLAN will listen on. On most Linux servers, this is “eth0”, but you can quickly check by typing ifconfig
at the Linux shell prompt and selecting the appropriate name which has an IP address matching your host server.
This latter information is fairly important because you need to make sure the subnet
and gateway
match up to the same network. The subnet is simply the 255 addresses in your local network, recorded in CIDR format. Just set the subnet
variable to whatever your network is (e.g. “192.168.1.0/24).
The gateway
is quite often your router and will most likely be the second IP address in your subnet (e.g. “192.168.1.1”).
The ip_range
is set to only cover to IP addresses, starting with that of Pi-Hole. By using the CIDR notation of /31, it means that only those two addresses will be available on the MACVLAN.
dnsnet:
name: dnsnet
driver: macvlan
driver_opts:
parent: ${ETH}
ipam:
config:
- subnet: ${SUBNET}
gateway: ${GATEWAY}
ip_range: ${PIHOLE_IP}/31
The Docker Compose Script in its Entirety
Put together, you have a Docker Compose script and a .env
file which will stand up Pi-Hole and call Cloudflare (or whichever DoH server you prefer) over HTTPs.
Once it is set up and you have configured Pi-Hole to pull the appropriate block lists (there is a regularly updated list of lists at https://blocklists.info/), you can point your router (and in my case, my Synology DNS resolver as well) to your Pi-Hole IP address. Any DNS requests made in your local network and from your Synology NAS will be routed through Pi-Hole and onward (if not blocked) to the DoH server of your choice.
The Final Docker Compose Script
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
hostname: ${HOST}
domainname: ${DOMAIN_NAME}
mac_address: ec:32:45:4d:01:a1
networks:
dnsnet:
ipv4_address: ${PIHOLE_IP}
cap_add:
- NET_ADMIN
dns:
- 127.0.0.1
- 1.1.1.1
depends_on:
cloudflared:
condition: service_healthy
environment:
TZ: ${TIMEZONE}
ServerIP: ${PIHOLE_IP}
PIHOLE_DNS_: '${CLOUDFLARED_IP}#53'
VIRTUAL_HOST: ${HOST}.${DOMAIN_NAME}
volumes:
- '/docker/appdata/pihole/:/etc/pihole/'
- '/docker/appdata/pihole/dnsmasq.d/:/etc/dnsmasq.d/'
restart: unless-stopped
cloudflared:
container_name: cloudflared
image: visibilityspots/cloudflared:latest
networks:
dnsnet:
ipv4_address: ${CLOUDFLARED_IP}
environment:
TZ: ${TIMEZONE}
PORT: 53
ADDRESS: 0.0.0.0
UPSTREAM1: https://1.1.1.1/dns-query
UPSTREAM2: https://1.0.0.1/dns-query
restart: unless-stopped
networks:
dnsnet:
name: dnsnet
driver: macvlan
driver_opts:
parent: ${ETH}
ipam:
config:
- subnet: ${SUBNET}
gateway: ${GATEWAY}
ip_range: ${PIHOLE_IP}/31
The Docker Compose Environment Variables
CLOUDFLARED_IP=192.168.1.9
DOMAIN_NAME=domain.com
ETH=eth0
GATEWAY=192.168.1.1
HOST=dns
PIHOLE_IP=192.168.1.8
SUBNET=192.168.1.0/24
TIMEZONE=Europe/London
0 Comments