DEV Community

Cover image for Ultimate DNS Shield — Self-hosted recursive DNS with Pi-hole + Unbound + Docker
Cherif Jebali
Cherif Jebali

Posted on • Originally published at cherifjebali.com

Ultimate DNS Shield — Self-hosted recursive DNS with Pi-hole + Unbound + Docker

Every domain you resolve goes through a DNS resolver. By default, that's Google (8.8.8.8), Cloudflare (1.1.1.1), or your ISP (all of which log your queries). Your browsing history is essentially visible to them.

Ultimate DNS Shield breaks that dependency entirely. Unbound resolves domains by querying root nameservers directly. Pi-hole drops ad and tracker domains before they reach the network stack. No third-party resolver ever sees your queries.

This is not about hiding from law enforcement; it's about reducing unnecessary data exposure to commercial entities and your ISP.

Architecture

Three layers, all containerised on an isolated Docker bridge network (dns-net). Unbound is never exposed to the LAN (only Pi-hole can reach it).

LAN devices ──► Pi-hole :53 (172.20.0.3)
                    │
                    └──► Unbound :5335 (172.20.0.2) (internal only)
                                │
                                └──► Root nameservers (a-m.root-servers.net)
Enter fullscreen mode Exit fullscreen mode
networks:
  dns-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24

services:
  unbound:
    image: mvance/unbound:latest
    networks:
      dns-net:
        ipv4_address: 172.20.0.2

  pihole:
    image: pihole/pihole:latest
    networks:
      dns-net:
        ipv4_address: 172.20.0.3
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
Enter fullscreen mode Exit fullscreen mode

Pi-hole forwards unblocked queries to Unbound via PIHOLE_DNS_=172.20.0.2#5335 (not a public resolver).

Prerequisites

  • Raspberry Pi 4 (2 GB RAM minimum)
  • Raspberry Pi OS Lite 64-bit
  • Static LAN IP on the Pi
  • Docker + Docker Compose installed
  • Port 53 free on the host

Watch out: systemd-resolved occupies port 53 by default on Ubuntu/Pi OS. Disable it first: sudo systemctl disable --now systemd-resolved

Installation

git clone https://github.com/cherifon/Ultimate-DNS-Shield.git
cd Ultimate-DNS-Shield
Enter fullscreen mode Exit fullscreen mode

Edit config/docker-compose.yml:

environment:
  TZ: Europe/Paris
  WEBPASSWORD: your_password
volumes:
  - /home/pi/pihole:/etc/pihole
  - /home/pi/dnsmasq:/etc/dnsmasq.d
Enter fullscreen mode Exit fullscreen mode
# Preview (no changes applied)
sudo bash scripts/install.sh --dry-run

# Full install
sudo bash scripts/install.sh
Enter fullscreen mode Exit fullscreen mode

Point your router's DHCP DNS to the Pi's LAN IP (every device is covered automatically).

Security hardening

sudo bash scripts/security.sh
Enter fullscreen mode Exit fullscreen mode
  • UFW (port 53 and 80 from LAN only; everything else denied)
  • Fail2Ban (rate-limits DNS floods and web UI brute-force)
  • DNSSEC (enforced in Unbound; invalid signatures rejected)
  • Unbound isolation (bound to 172.20.0.2 only; unreachable from outside Docker's bridge)

Verification

# Normal resolution (answered by your Pi)
dig github.com @<PI_IP>

# Ad blocking (returns 0.0.0.0)
dig doubleclick.net @<PI_IP>

# DNSSEC (invalid signature correctly rejected)
dig sigfail.verteiltesysteme.net @<PI_IP>
Enter fullscreen mode Exit fullscreen mode

Results

  • No DNS query resolved by a third party
  • Ad and tracker domains blocked network-wide
  • DNSSEC enforced
  • First-query latency ~8ms on LAN; cached queries <1ms
  • Full stack managed via two scripts

GitHub: cherifon/Ultimate-DNS-Shield

Originally published on cherifjebali.com

Top comments (0)