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:

  1. The pi-hole web interface (/admin) is proxied via Traefik
  2. All pi-hole’s traffic for DNS queries is not proxied via Traefik

A schematic of above goals is shown in below topology:

Docker topology with Traefik and pi-hole

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.