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.
DNS is the most overlooked piece of homelab security. You can harden every container, lock down every reverse proxy route, and still be leaking every domain your network touches to a third-party resolver. Running your own recursive DNS with Pi-hole and Unbound keeps that data local and gives you a policy enforcement point that most homelab setups skip entirely.
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(tcpandudp), 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.
What is Pi-hole?
Pi-hole is a network-level ad blocker that acts as your LAN’s DNS sinkhole. Point your clients or router at it, and it filters out ad and tracking domains before they ever resolve. It also gives you a clean admin dashboard for monitoring query traffic across your entire network. For a deeper introduction to both tools, see our post on Pi-hole + Unbound and why they replace your ISP’s DNS.
What is Unbound?
Unbound is a validating, recursive DNS resolver. Instead of forwarding your queries to Google or Cloudflare, Unbound resolves them directly against authoritative nameservers. Paired with Pi-hole, it means your DNS queries never leave your network until they hit the root servers — no third-party resolver sees your full query history.
Evidence used
This post is based on:
docker/pihole-unbound/compose.yamldocker/pihole-unbound/etc-pihole/pihole.tomldocker/pihole-unbound/etc-pihole/dnsmasq.confdocs/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:
- DNS is intentionally bound on host port
53for LAN clients. - 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.
Are you still forwarding to a public resolver like 1.1.1.1 or 8.8.8.8? If so, consider what you gain by switching to local recursion with Unbound — and what tradeoffs in latency and reliability you might need to plan for.
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
- Router + middleware labels can simplify UX without adding app complexity.
- DNS security is mostly network security; TLS on the admin panel does not protect an exposed resolver.
- Pi-hole config drift is easy when generated files and source-of-truth files diverge. Treat
pihole.tomlas primary. - Pinning an image tag is good; patch cadence still matters for infrastructure services.
What I’d do differently
- Restrict DNS ingress to trusted LAN segments only at firewall/router level instead of relying on container defaults.
- Revisit
listeningMode = "ALL"; prefer the least permissive mode that still serves required clients. - Add explicit access control in front of admin UI (for example, Authelia or IP allowlists), not just host-based routing.
- Keep capabilities minimal and validate whether all three (
NET_ADMIN,SYS_NICE,SYS_TIME) are truly required in this environment. - Add a periodic config audit that compares intended settings (
pihole.toml) against effective runtime behavior.
Security notes
- Exposing host port
53is 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.
Summary
- Pi-hole + Unbound in a single container gives you ad-blocking and recursive DNS without relying on any third-party resolver.
- Traefik redirect middleware cleanly routes the admin UI without app-side rewrites or exposing extra host ports.
- Host port
53is network-infrastructure exposure — treat it accordingly with firewall rules, not just container defaults. listeningMode = "ALL"and broad Linux capabilities are workable starting points, but both need tightening for a hardened setup.- Config drift between
pihole.tomland generated files is a real operational risk. Treat the TOML file as your source of truth.
Share Your DNS Stack
How are you handling DNS in your homelab? Running Pi-hole with Unbound, forwarding to a public resolver, or something else entirely? If you have found good solutions for the latency tradeoffs of local recursion or have tips on locking down listeningMode, share what has worked for you.