🚀 Executive Summary

TL;DR: Pi-hole and Unbound in Docker can create a recursive DNS loop where containers try to resolve DNS through each other, leading to network outages. The recommended solution involves explicitly configuring Pi-hole’s DNS in `docker-compose.yml` to point directly to Unbound’s static IP on a custom Docker network, breaking the loop permanently.

🎯 Key Takeaways

  • The ‘DNS Death Loop’ occurs when Pi-hole inherits the host’s DNS (pointing to Pi-hole), and then Pi-hole uses Unbound, which also inherits the host’s DNS, creating a circular dependency.
  • The permanent fix involves using the `dns` key in `docker-compose.yml` for the Pi-hole service, explicitly pointing it to Unbound’s static IP address within a custom Docker bridge network.
  • Host networking (`network_mode: “host”`) can simplify DNS by removing Docker’s network abstraction, but it sacrifices port flexibility and reduces security isolation between containers and the host.

Pi-hole and Unbound not working together in Docker

Tired of your Pi-hole and Unbound containers creating a DNS black hole in Docker? This guide diagnoses the common recursive loop issue and provides three battle-tested solutions to get your network-wide ad-blocking and recursive DNS working in harmony.

When Docker Networking Bites Back: Untangling Pi-hole and Unbound

It was 2 AM. Of course it was. PagerDuty was screaming about our entire staging environment being down. The weird part? I could SSH into every single host. The VMs were up, the metal was fine. Yet, nothing could talk to anything else. `curl prod-api-svc` just hung forever. After ten minutes of frantic checking, I found the culprit: DNS. A brand new Pi-hole and Unbound stack, deployed by a junior engineer that afternoon, had created a perfect, silent, and catastrophic DNS loop that took down our entire internal resolution. Sound familiar? If you’re wrestling with getting these two to play nice inside Docker, you’ve come to the right place. Let’s get you sorted.

The “Why”: Understanding the Docker DNS Death Loop

Before we fix it, you need to understand the trap you’ve fallen into. It’s a classic case of “who’s on first?” for network resolution:

  1. Your Host Machine is configured to use Pi-hole for DNS (e.g., its `/etc/resolv.conf` points to `127.0.0.1` where Pi-hole’s port is mapped).
  2. By default, your Pi-hole Docker Container inherits the host’s DNS settings. It tries to use itself for DNS.
  3. You configured Pi-hole to use your Unbound Container as its upstream DNS server.
  4. The Unbound container, also inheriting the host’s settings, then tries to resolve its requests by asking… you guessed it, the Pi-hole Container.

You’ve created a closed circuit where every DNS query is passed in a circle until it times out. The core issue is that Docker’s default networking is trying to be helpful by passing the host’s resolver configuration to the container, but in this specific scenario, it’s poison.

Fix #1: The “Get Me Out of This PagerDuty Alert” Quick Fix

This is the hacky, temporary solution you use when everything is on fire and you just need to get things working right now. We’re going to manually force the Pi-hole container to use a different DNS server by editing its `resolv.conf` file directly.

First, get a shell inside your Pi-hole container:

docker exec -it pihole /bin/bash

Next, edit the resolver configuration file. You can use `nano` or `vi` if they are installed, or a simple `echo` command to overwrite it:

echo "nameserver 1.1.1.1" > /etc/resolv.conf

This command tells the Pi-hole container to stop using the host’s resolver (which points back to itself) and instead use Cloudflare’s public DNS directly for its own outbound requests (like updating gravity lists). Your Pi-hole will still use Unbound for the queries it processes from your LAN clients, but the container itself now has a sane way to get to the outside world.

Warning: This is a temporary fix! The moment you restart or recreate the Pi-hole container, this file will be reset, and your DNS loop will return. Use this to stop the bleeding, then move on to the permanent fix.

Fix #2: The “Do It Right” Permanent Solution with Docker Compose

This is the solution I recommend for 99% of setups. We’re going to explicitly tell the Pi-hole service what DNS servers it should use, right in the `docker-compose.yml` file. This survives restarts and is the clean, declarative way to manage infrastructure.

In your `docker-compose.yml`, find your `pihole` service definition and add the `dns` key. You should point it directly to your Unbound container’s IP address on the Docker network, and maybe add a public DNS as a backup.

services:
  pihole:
    image: pihole/pihole:latest
    # ... your other settings like ports, volumes, etc.
    environment:
      - TZ=America/New_York
      - WEBPASSWORD=your_secure_password
    volumes:
      - './etc-pihole/:/etc/pihole/'
      - './etc-dnsmasq.d/:/etc/dnsmasq.d/'
    # THIS IS THE MAGIC:
    dns:
      - 172.20.0.10 # <-- The IP of your Unbound container
      - 1.1.1.1     # <-- A public DNS as a fallback
    cap_add:
      - NET_ADMIN
    restart: unless-stopped

  unbound:
    image: mvance/unbound:latest
    # ... your other unbound settings
    # NOTE: We give unbound a static IP so pihole can find it.
    networks:
      default:
        ipv4_address: 172.20.0.10

# Define the network so we can assign static IPs
networks:
  default:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24

In this example, we've created a custom Docker network, given the `unbound` container a static IP (`172.20.0.10`), and then explicitly told the `pihole` container to use that IP for its DNS. This breaks the loop permanently and correctly.

Fix #3: The "Network Overhaul" Option (Host Networking)

Sometimes, you just want to strip away Docker's network abstraction entirely. This is the "big hammer" approach. By setting the network mode to `host`, you tell the containers to share the host machine's network stack directly. They will no longer be isolated in their own network namespace.

Pros

  • Simplicity: DNS resolution becomes dead simple. The Pi-hole container can just point to `127.0.0.1:5335` (or whatever port Unbound is listening on), just as if it were a regular process on the host.
  • Performance: You eliminate the Docker network bridge overhead, which can result in a marginal performance increase.

Cons

  • Port Conflicts: This is the big one. If another service on your host is using port 53, 80, or 443, your Pi-hole container will fail to start. You lose the port-mapping flexibility of bridged networking.
  • Reduced Security: You are tearing down the network isolation between your containers and the host. A vulnerability in one container could more easily impact the host system.

To implement this, you would modify your `docker-compose.yml` like so:

services:
  pihole:
    image: pihole/pihole:latest
    # ... other settings
    network_mode: "host"
    restart: unless-stopped

  unbound:
    image: mvance/unbound:latest
    # ... other settings
    network_mode: "host"
    restart: unless-stopped

My Take: I personally avoid `host` networking unless absolutely necessary. The declarative `dns` key in Fix #2 provides the same reliability without sacrificing the security and flexibility of Docker's bridge networking. But, for a simple home server where you control everything, this can be a viable and easy-to-understand option.

At the end of the day, that 2 AM outage taught me a valuable lesson: always be explicit with your container's DNS settings. Don't trust the defaults when you're building a system that provides the very service—DNS—that all other systems depend on. Choose the right fix for your situation, apply it, and get back to enjoying a fast, private, and ad-free network.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why do Pi-hole and Unbound create a DNS loop in Docker?

The loop occurs because Pi-hole, by default, inherits the host's DNS settings (which often point back to Pi-hole itself), and then Pi-hole is configured to use Unbound, which also inherits the host's DNS, causing a recursive lookup chain that times out.

âť“ How does the Docker Compose `dns` configuration compare to using host networking for Pi-hole and Unbound?

The Docker Compose `dns` key provides explicit, declarative DNS resolution within a custom bridge network, maintaining container isolation and port flexibility. Host networking simplifies DNS by sharing the host's network stack but sacrifices isolation and can lead to port conflicts.

âť“ What is a common implementation pitfall when setting up Pi-hole and Unbound in Docker?

A common pitfall is the 'DNS Death Loop' where containers recursively query each other for DNS. This is resolved by explicitly configuring Pi-hole's `dns` setting in `docker-compose.yml` to point directly to Unbound's static IP within a custom Docker network, preventing circular lookups.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading