Pi-hole + Unbound Behind Traefik with a Clean /admin Redirect

How this homelab publishes Pi-hole admin via Traefik while keeping DNS local, with practical hardening steps for the risky defaults.

Tools

TL;DR

  • This stack runs Pi-hole + Unbound in one container (mpgirro/pihole-unbound:2025.11.1) and exposes only the admin UI through Traefik.
  • A Traefik redirect middleware sends / to /admin/, so users land in the right place without app-side rewrite logic.
  • DNS still binds to host port 53 (tcp and udp), so firewall boundaries matter more than reverse-proxy polish.
  • Security posture is workable but unfinished: listeningMode = "ALL", query logging retention, and broad Linux capabilities need tighter control.

Evidence used

This post is based on:

  • docker/pihole-unbound/compose.yaml
  • docker/pihole-unbound/etc-pihole/pihole.toml
  • docker/pihole-unbound/etc-pihole/dnsmasq.conf
  • docs/deployment-guide.md

What this design is doing

The container handles both ad-blocking DNS and recursive resolution:

services:
  pihole:
    image: mpgirro/pihole-unbound:2025.11.1
    ports:
      - '53:53/tcp'
      - '53:53/udp'
    cap_add:
      - NET_ADMIN
      - SYS_NICE
      - SYS_TIME

Two things matter here:

  1. DNS is intentionally bound on host port 53 for LAN clients.
  2. The admin UI is not published with a host port in Compose; it is routed through Traefik.

Admin UI through Traefik + redirect middleware

The Compose labels include both the main router and a root-path redirect:

labels:
  - traefik.http.routers.pihole.rule=Host(`pihole.subdepthtech.org`)
  - traefik.http.routers.pihole.entrypoints=https
  - traefik.http.routers.pihole.tls=true
  - traefik.http.services.pihole.loadbalancer.server.port=80
  - traefik.http.routers.pihole-root.rule=Host(`pihole.subdepthtech.org`) && Path(`/`)
  - traefik.http.routers.pihole-root.middlewares=pihole-admin-redirect
  - traefik.http.routers.pihole-root.priority=100
  - traefik.http.middlewares.pihole-admin-redirect.redirectregex.regex=^.*$
  - traefik.http.middlewares.pihole-admin-redirect.redirectregex.replacement=https://pihole.subdepthtech.org/admin/
  - traefik.http.middlewares.pihole-admin-redirect.redirectregex.permanent=true

This is practical and clean: users do not need to remember /admin/, and the redirect is explicit in edge routing.

DNS behavior from repo config

From pihole.toml and generated dnsmasq.conf, this instance is configured to recurse through local Unbound:

[dns]
upstreams = ["127.0.0.1#5335"]
listeningMode = "ALL"
queryLogging = true

And the generated dnsmasq config confirms:

no-resolv
server=127.0.0.1#5335
port=53
# Listen on all interfaces, permit all origins
except-interface=nonexisting

So the architecture is coherent: Pi-hole is the policy engine, Unbound is the upstream resolver, and no public upstream resolvers are used in this path.

Deployment and verification commands

Using the repo’s deployment workflow:

./scripts/deploy.sh all --dry-run
./scripts/deploy.sh pihole-unbound

Quick checks after deploy:

# Container state

docker --context homelab-remote compose -f docker/pihole-unbound/compose.yaml ps

# DNS answer path

dig @<lan-dns-ip> example.com

# Admin route + redirect

curl -Ik https://pihole.subdepthtech.org/
curl -Ik https://pihole.subdepthtech.org/admin/

Lessons learned

  1. Router + middleware labels can simplify UX without adding app complexity.
  2. DNS security is mostly network security; TLS on the admin panel does not protect an exposed resolver.
  3. Pi-hole config drift is easy when generated files and source-of-truth files diverge. Treat pihole.toml as primary.
  4. Pinning an image tag is good; patch cadence still matters for infrastructure services.

What I’d do differently

  1. Restrict DNS ingress to trusted LAN segments only at firewall/router level instead of relying on container defaults.
  2. Revisit listeningMode = "ALL"; prefer the least permissive mode that still serves required clients.
  3. Add explicit access control in front of admin UI (for example, Authelia or IP allowlists), not just host-based routing.
  4. Keep capabilities minimal and validate whether all three (NET_ADMIN, SYS_NICE, SYS_TIME) are truly required in this environment.
  5. Add a periodic config audit that compares intended settings (pihole.toml) against effective runtime behavior.

Security notes

  • Exposing host port 53 is high impact; treat it as network-infrastructure exposure, not just another app port.
  • listeningMode = "ALL" can become an open-resolver risk if perimeter controls are weak.
  • Query logging is useful for troubleshooting but increases retention sensitivity; define a retention policy.
  • Traefik protects the admin UI transport path, but DNS traffic itself is still unauthenticated UDP/TCP unless you explicitly add encrypted DNS layers.
  • Avoid publishing private host/IP mappings in public docs; use sanitized examples (10.x.x.x) for write-ups.