How to Set Up Pi-hole v6 with Unbound on Raspberry Pi Using Docker

 · 5 min read  ·

Pi-hole is a powerful network-wide ad blocker that runs on your local network. Pairing it with Unbound—a recursive DNS resolver—gives you complete control over DNS queries without relying on third-party DNS providers like Google or Cloudflare.

This guide shows you how to deploy Pi-hole v6 (which is very much different from v5) with Unbound on a Raspberry Pi using Docker Compose.

Prerequisites

Raspberry Pi (3, 4, or 5) running Ubuntu Server LTS with Docker/Docker Compose installed.

💡

If you don’t have a Raspberry Pi, check out my complete guide on buying your first Raspberry Pi to get started.

Step 1: Disable systemd-resolved

Pi-hole needs port 53, which is used by systemd-resolved by default. Disable and stop it:

sudo systemctl disable systemd-resolved
sudo systemctl stop systemd-resolved

This frees up port 53, but it also breaks DNS for the Pi itself. On Ubuntu, /etc/resolv.conf is a symlink to the systemd-resolved stub resolver at 127.0.0.53, which you just shut down, so the Pi can no longer resolve hostnames. That’s a problem, because in the next steps you’ll need working DNS to pull the Docker images.

For now, give the Pi a temporary working resolver so the rest of the setup goes smoothly. We’ll point it at Pi-hole itself once the stack is up (see Step 8):

# Remove the broken symlink
sudo rm /etc/resolv.conf

# Temporarily point the Pi at an upstream resolver so we can pull images
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
echo "nameserver 8.8.8.8" | sudo tee -a /etc/resolv.conf

Step 2: Create Directory Structure

It’s good practice to deploy applications in the /srv folder (on your Pi).

mkdir -p /srv/docker/pihole/unbound
cd /srv/docker/pihole

Step 3: Create Unbound Configuration

Create /srv/docker/pihole/unbound/unbound.conf:

server:
    # Port to listen on (non-standard to avoid conflict with Pi-hole)
    port: 5335

    # Listen on all interfaces (accessible within shared network namespace)
    interface: 0.0.0.0

    # Use IPv4 only
    do-ip4: yes
    do-ip6: no
    do-udp: yes
    do-tcp: yes

    # Trust glue only if it is within the servers authority
    harden-glue: yes

    # Require DNSSEC data for trust-anchored zones
    harden-dnssec-stripped: yes

    # Don't use Capitalization randomization
    use-caps-for-id: no

    # Reduce EDNS reassembly buffer size
    edns-buffer-size: 1232

    # Perform prefetching of close to expired message cache entries
    prefetch: yes

    # Number of threads to create
    num-threads: 1

    # Power of 2 close to num-threads
    so-rcvbuf: 1m

    # Ensure privacy of local IP ranges
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10

These values ☝️, if you’re wondering where they came from, are straight from the docs.

Step 4: Create Docker Compose File

Create /srv/docker/pihole/docker-compose.yml:

services:
  pihole:
    image: pihole/pihole:2025.11.1
    container_name: pihole
    restart: unless-stopped

    # Use host networking for DNS (port 53)
    network_mode: host

    environment:
      - TZ=America/New_York
      - FTLCONF_webserver_api_password=changeme123
      # - FTLCONF_webserver_domain=pihole.yourdomain.com -- If you plan to have a custom domain for the web interface
      - FTLCONF_LOCAL_IPV4=192.168.1.92 # change me
      - FTLCONF_dns_upstreams=127.0.0.1#5335;127.0.0.1#5335

    volumes:
      - ./etc-pihole:/etc/pihole
      - ./etc-dnsmasq.d:/etc/dnsmasq.d

    cap_add:
      - NET_ADMIN

  unbound:
    image: alpinelinux/unbound:latest
    container_name: unbound
    restart: unless-stopped

    # Share Pi-hole's network stack
    network_mode: service:pihole

    volumes:
      - ./unbound/unbound.conf:/etc/unbound/unbound.conf:ro

Important: Update these values:

  • TZ: Your timezone (list here)
  • FTLCONF_webserver_api_password: Your desired admin password
  • FTLCONF_LOCAL_IPV4: Your Raspberry Pi’s static IP address

Step 5: Deploy the Stack

cd /srv/docker/pihole
sudo docker compose up -d

Step 6: Verify Unbound Integration

Check that Pi-hole is forwarding queries to Unbound:

# Query a domain
dig google.com @127.0.0.1

# Watch live query log
sudo docker exec pihole pihole -t

You should see queries being forwarded to 127.0.0.1#5335 (Unbound) in the logs.

Step 7: Configure Your Network

Point your router’s DNS settings to your Pi’s IP address (e.g., 192.168.1.92), or configure individual devices to use it as their DNS server.

Step 8: Point the Pi at Its Own DNS

Back in Step 1 we gave the Pi a temporary upstream resolver just to get the images pulled. Now that Pi-hole is running, switch the host to resolve through your own stack (Pi-hole → Unbound), then make /etc/resolv.conf immutable so nothing overwrites it on reboot:

# Remove the immutable flag if it's already set
sudo chattr -i /etc/resolv.conf 2>/dev/null

# Point the Pi at Pi-hole itself
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf

# Lock the file so it survives reboots
sudo chattr +i /etc/resolv.conf

Now the Pi’s own DNS lookups are ad-blocked and resolved recursively too—no third-party resolver involved, not even for the host. (This also means the Pi depends on Pi-hole being up to resolve names. If you ever stop the container for maintenance, temporarily set the nameserver back to an upstream like 1.1.1.1.)

Key Configuration Notes

Pi-hole v6 Environment Variables

Pi-hole v6 uses FTLCONF_* environment variables instead of the old v5 variables:

  • PIHOLE_DNS_ (deprecated)
  • FTLCONF_dns_upstreams (use this)

Network Mode

We use network_mode: host for Pi-hole so it can bind to port 53, and network_mode: service:pihole for Unbound so it shares Pi-hole’s network namespace. This allows them to communicate via 127.0.0.1.

Persistent Data

Your Pi-hole configuration is stored in ./etc-pihole, which persists across container restarts and updates.

Troubleshooting

DNS Not Working

Verify systemd-resolved is disabled:

sudo systemctl status systemd-resolved

The Pi Itself Has No Internet

If the Pi can serve DNS to other devices but can’t reach the internet on its own (e.g., ping google.com fails, apt update hangs), its /etc/resolv.conf is still pointing at the disabled systemd-resolved stub. Confirm what it contains:

cat /etc/resolv.conf

If you see nameserver 127.0.0.53, your /etc/resolv.conf is still on the dead systemd-resolved stub. Redo the resolv.conf fix from Step 8 (or, if the stack isn’t running yet, the temporary upstream from Step 1).

Unbound Not Connecting

Check if Unbound is running:

sudo docker logs unbound

Web Interface Not Accessible

Access Pi-hole’s web interface at http://YOUR_PI_IP/admin (e.g., http://192.168.1.92/admin)

Updating

To update Pi-hole and Unbound:

cd /srv/docker/pihole
sudo docker compose pull
sudo docker compose up -d

Your configuration and data will persist across updates.

Conclusion

You now have a privacy-focused, ad-blocking DNS server running on your Raspberry Pi. All DNS queries are resolved recursively via Unbound without relying on third-party DNS providers, giving you complete control and privacy over your network’s DNS traffic.

This page may contain affiliate links. Please see my affiliate disclaimer for more info.

Related Posts

View All Posts »