Pihole is a great tool to protect your home network from trackers and annoying ads. It can be deployed either directly on a server or in a Docker container. I personally lean toward using Docker whenever possible for the flexibility and isolation it provides. Services deployed in Docker containers are significantly easier to migrate than raw installations and Pihole is no exception.
Running Pihole on Docker is pretty straightforward, but things start to get a bit complicated when it comes to enabling DHCP - using Pihole to serve DHCP requests. With Docker’s default bridge network mode, we can’t use Pihole as a DHCP server. So we have two options: (1) use the host network mode, or (2) run a DHCP relay (more on this later).
Option (1) is the easiest because all we’d have to do is set
network_mode: "host" in the service definition, but it comes with a considerable
disadvantage. It undermines the network isolation provided by Docker’s bridge
driver, not to mention that it will take up ports from the host’s network which
could be a concern depending on what’s running on your server. Option
(2) allows us to stick to the bridge mode but adds a good amount of complexity.
If you’re anything like me, you’ll favour security and try to maintain Docker’s
Option (2) it is. Make sure your cup of coffee is full, and let’s dive in!
Pihole on Docker
First, let’s build a
docker-compose.yml file with a basic configuration to run
version: "3.2" services: pihole: container_name: pihole image: pihole/pihole:latest ports: - "53:53/tcp" - "53:53/udp" - "8080:80/tcp" environment: - TZ=America/New_York - WEBPASSWORD=very-secure-password # Choose a password for admin - PIHOLE_DNS_="18.104.22.168;22.214.171.124" # Set upstream DNS servers - ServerIP="10.10.0.10" # Your host's external IP volumes: - pihole:/etc/pihole/ - dnsmasqd:/etc/dnsmasq.d/ restart: unless-stopped volumes: pihole: dnsmasqd:
In this example, we define a service called
pihole using the official Pihole
Docker image. Then, we publish few ports to get DNS requests (UDP port 53)
forwarded to the container running Pihole, and to allow access to the web panel
(TCP port 80). After that, we set some environment variables:
WEBPASSWORD1: The password to use when logging into the admin panel.
PIHOLE_DNS_: A semicolon separated list of upstream DNS servers. These are the servers Pihole will reach out to in order to resolve DNS queries.
ServerIP: The external host IP. This is the IP that machines on the network will send their DNS queries to. In other words, this should be the hosts IP on the LAN network.
Last, we define a couple of volumes to persist configuration across multiple container restarts.
DHCP through Docker’s Bridge Network
At this point, we have a working Pihole deployment ready to manage DNS on the host’s network. Since we didn’t specify the network mode in the docker-compose file, the container is provisioned using the default driver: bridge network. This means that Pihole lives in a separate network than the host along with other machines on the LAN. Docker daemon bridges these 2 networks and forwards ports as specified in the docker-compose file. This works great for DNS, but not so much for DHCP. If we tried to enable DHCP on Pihole’s web interface, it would unfortunately not work. To understand why, let’s go over a brief overview of how DHCP works.
When a client joins a network it starts broadcasting a DHCP discovery request on that same network on UDP port 67. If there’s a DHCP server listening, it will respond to this request with a lease offer that the client then accepts2. This is how machines get assigned IP addresses dynamically when joining a network.
DHCP is not routable however, so discovery requests don’t span across different networks. In our setup, since Pihole is running in a Docker container with bridge network mode, it lives in a separate network (Docker internal network) than the LAN, so DHCP discovery requests stop at the edge of the LAN and never make it to Pihole. To solve this, we need a DHCP relay connected to both the LAN and Docker’s internal network. It will intercept discovery requests on one network and forward them to Pihole on the other.
With the theory out of the way, let’s move on to a practical solution to our
problem. We will use
dhcp-helper as a DHCP relay. First, we create an small
image with that package and place the
Dockerfile in a folder called
FROM alpine:latest RUN apk --no-cache add dhcp-helper EXPOSE 67 67/udp ENTRYPOINT ["dhcp-helper", "-n"]
The file content is self explanatory. We’re using an
alpine image, adding the
dhcp-helper package, exposing the UDP port 67 in order to receive DHCP
discovery requests, and we’re running the helper command at startup.
The next step is to update the original Pihole docker-compose file to include the DHCP relay. We will need to add a service for the relay, an additional network with a fixed IP for the Pihole container, and make the Pihole service depend on the DHCP relay.
version: "3.2" services: pihole: container_name: pihole # ... dhcphelper: build: ./dhcp-helper restart: unless-stopped network_mode: "host" command: -s 172.31.0.111 cap_add: - NET_ADMIN
Notice that we’re using the
host network mode for the DHCP relay service. This
is important if we want to intercept discovery requests on the same network as
the host (LAN). Also, we’re adding a flag to the entry point command specifying
the server’s IP. We’ll assign this IP to Pihole’s service as follows:
version: "3.2" services: pihole: container_name: pihole # ... networks: backend: ipv4_address: '172.31.0.111'
In order to use this new network that we named
backend we need to define it in
our docker-compose file:
version: "3.2" services: pihole: container_name: pihole # ... dhcphelper: build: ./dhcp-helper # ... networks: backend: ipam: config: - subnet: 172.31.0.0/16
With all these modifications, we end up with the following docker-compose file:
version: "3.2" services: pihole: container_name: pihole image: pihole/pihole:latest ports: - "53:53/tcp" - "53:53/udp" - "8080:80/tcp" environment: - TZ=America/New_York - WEBPASSWORD=very-secure-password # Choose a password for admin - PIHOLE_DNS_="126.96.36.199;188.8.131.52" # Set upstream DNS servers - ServerIP="10.10.0.10" # Your host's external IP volumes: - pihole:/etc/pihole/ - dnsmasqd:/etc/dnsmasq.d/ restart: unless-stopped depends_on: - dhcphelper cap_add: - NET_ADMIN networks: backend: ipv4_address: '172.31.0.111' dhcphelper: build: ./dhcp-helper restart: unless-stopped network_mode: "host" command: -s 172.31.0.111 cap_add: - NET_ADMIN networks: backend: ipam: config: - subnet: 172.31.0.0/16 volumes: pihole: dnsmasqd:
One More Thing
So far, we’ve managed to:
- Run a Pihole container with bridge network mode
- Run a DHCP relay container that listens for discovery requests on the LAN and forwards them to Pihole on Docker’s internal network.
However, by default Pihole sends out leases instructing clients to use its IP as
a DNS server. The IP used here is Pihole’s IP address on the network where it
received the DHCP request. This means that the clients will be configured to
send their DNS queries to an address on the network
172.31.0.0/16 and this
will not work. This is an internal Docker network that we created to connect
Pihole to the DHCP realy, and that machines on the LAN have no access to. In
order to resolve this, Pihole needs to grant DHCP leases with the host’s
external IP as the DNS. we can accomplish this by adding a configuration option
dnsmasq running inside Pihole’s container. This is as simple as creating a
file inside Pihole’s container under
/etc/dnsmasq.d/ with the following:
To make our lives easier, let’s put this in a script file called
update-dhcp-dns and also support multiple DNS servers while we’re at it.
#!/bin/bash DNS_SERV_IPS="" # A comma separated list of DNS IPs. E.g.: "10.10.0.10,184.108.40.206,220.127.116.11" if [[ -z $DNS_SERV_IPS ]]; then echo "Please set DNS servers IPs by modifying the script file." exit 1; fi docker-compose exec pihole bash -c "echo 'dhcp-option=option:dns-server,$DNS_SERV_IPS' > /etc/dnsmasq.d/07-dhcp-options"
We finally have all the pieces we need to run Pihole in a Docker container with
bridge network mode and have it serve DHCP requests on the host’s LAN. After
update-dhcp-dns with your desired
configuration values, like the web password, server IP, and DNS IPs, you can
deploy using the following commands.
docker-compose up -d pihole ./update-dhcp-dns
Note that you only need to run
./update-dhcp-dns once. The modifications
introduced by this script will persist across container restarts because we’re
using a Docker volume in Pihole’s service definition.
This is actually done in two steps: the client sends a lease request to the server, then the server replies with an acknowledgement. ↩︎
If this environment variable is not set, Pihole will generate a random password. In this case, you’ll need to check the logs and search for the word “random” in order to find the generated password. ↩︎