Pi-hole behind Traefik
This post guides you through the config to put pi-hole’s web interface behind Traefik. For all local (home) services I use Traefik as a reverse proxy, which eases my configuration. However, pi-hole is not a simple web application serving plain HTML, as my local DNS server it has to respond to DNS queries over port 53. Traefik can proxy plain TCP and UDP, but in pi-hole’s case this is not a recommened practice.
The setup aims for two main goals:
- The pi-hole web interface (
/admin
) is proxied via Traefik - All pi-hole’s traffic for DNS queries is not proxied via Traefik
A schematic of above goals is shown in below topology:
The reason to split pi-hole’s traffic into two streams, is because of the major advantage Traefik gives you for web applications on one hand, however because of the docker/Traefik way of proxing, it also defeats an important aspect of the pi-hole so all DNS queries should never go through Traefik.
Ad 1: Traefik for all the web applications
First and foremost, Traefik gives you an enourmous advantage for all kind of web applications running from docker containers. You can have a single network interface listening to traffic; there is no need to bind docker ports to host ports, resulting you to remember what service is behind what port. It’s otherwise like the time of IP addresses without DNS: you just have to remember all the numbers, ugh.
Now, Traefik let you have different domains and it proxies them for you to the
right container. This way, my DNS is at dns1.myexample.com
instead of
myexample.com:1234
. I also have the Ubiquiti Unifi controller at
unifi.myexample.com
, Home Assistant at ha.myexample.com
and so on. If a
container exposes multiple ports, many of which you never might need, they are
just hidden from the outside.
Furthermore, I would like to have all web applications served via HTTPS, even when they are local services I use at home. For every application HTTPS is configured differently and centralizing cert generation at a single point (Traefik) is just such a breeze maintaining that stuff.
Ad 2: DNS queries outside of Traefik proxy
One of pi-hole’s features is logging; the dashboard shows what type of DNS queries are sent and by whom. Identifying clients is key here. In my setup I only have a default group of clients, but you can group clients by IP address or hostname and give them separate adlists to block too.
To get this working, pi-hole has to be able to identiy from which client the
request came. And here is the caveat: when you proxy DNS queries via Traefik,
these queries will be NAT. And pi-hole identifies every query coming from
localhost
. So all of the above will not be possible.
Configuration setup
To achieve both goals, there is a trick in Docker networking: in general, containers are attached to one or more networks. There is a default network, but for Traefik you attach all services to your internal Traefik network so nothing is exposed at host level and every network traffic is managed via Traefik.
Port bindings however, do bypass the attached networks. So the pi-hole container
is attached to my web
network, which is the internal Traefik network and
exposes ports 53
and 853
at host level!
$ docker inspect pihole
[
{
"NetworkSettings": {
"Ports": {
"53/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "53"
}
],
"53/udp": [
{
"HostIp": "0.0.0.0",
"HostPort": "53"
}
],
"67/udp": null,
"80/tcp": null,
"853/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "853"
}
]
},
"Networks": {
"web": {
"NetworkID": "95953b4e7294afe23a82ad20e3260809876d39e391d26870ccf8d12a40649da2",
"EndpointID": "581248f303703bddd27477590e83597134fd4889988ba77d44f34ba6a4bbe850",
"Gateway": "172.23.0.1",
"IPAddress": "172.23.0.2"
}
}
}
}
]
To get this running, make sure the container has only the internal Traefik
network and expose ports 53
and 853
as usual. This is my Ansible code to
deploy pi-hole containers:
- name: Create the pihole container
docker_container:
name: "{{ pihole_docker_container }}"
image: "{{ pihole_docker_tag }}"
restart_policy: unless-stopped
networks_cli_compatible: yes
networks:
- name: "{{ traefik_docker_network }}"
volumes:
- "{{ pihole_config_dir }}:/etc/pihole/"
- "{{ pihole_dnsmasq_dir }}:/etc/dnsmasq.d/"
ports:
- "53:53/tcp"
- "53:53/udp"
- "853:853"
labels:
traefik.enable: "true"
traefik.http.routers.pihole.entrypoints: "websecure"
traefik.http.routers.pihole.rule: "Host(`{{ pihole_public_domain }}`)"
traefik.http.routers.pihole.middlewares: "pihole-admin"
traefik.http.routers.pihole.service: "pihole"
traefik.http.routers.pihole.tls: "true"
traefik.http.routers.pihole.tls.certresolver: "le"
traefik.http.middlewares.pihole-admin.addprefix.prefix: "/admin"
traefik.http.routers.pihole_http.entrypoints: "web"
traefik.http.routers.pihole_http.rule: "Host(`{{ pihole_public_domain }}`)"
traefik.http.routers.pihole_http.middlewares: "redirect-to-https"
traefik.http.services.pihole.loadBalancer.server.port: "80"
Final tip: when using Traefik as reverse proxy, you get the benefit of
middleware. With the AddPrefix middleware I proxy dns1.myexample.com
directly
to the container’s /admin
URL path. So at https://dns1.myexample.com/
is my
dashboard located and /admin
is removed from the URL.