Pi-hole 6, DoH, and Docker

I recently upgraded both of my Pi-Holes to version 6.

Not much to really write about, but there are some differences for some edge use cases, none of which impacted myself.

I wanted to implement DNS over HTTPS (DoH), which adds an additional layer of security to your network.

If you are running Pi-hole on a Unifi UDM, see this post: https://www.nodinrogers.com/post/2025-09-15-pihole6-doh-unifi/

DNS query/response basics

You open your web browser and go to https://yahoo.com.

Your computer has no idea what a yahoo.com is, but it does know what an IP address is, and DNS translates hostnames (yahoo.com) to an IP address.

Your computer, under the hood, uses the IP address associated with yahoo.com and sends you on your merry way.

The problem with traditional DNS

However, DNS queries/responses are sent in plain text by default.

Easy to troubleshoot, but also easy to see/manipulate for any bad actor.

As you can see from the packet capture snippet below, DNS queries/responses are very easy to read.

Unencrypted DNS query/resposne

No big deal, but you have some Internet Service Providers (ISP) that track this information, and sell it to advertisers.

Worse, unencrypted DNS traffic can be intercepted and misdirected.

For example, if you wanted to go to your online bank website, and a bad actor intercepted the DNS query, they would provide an IP address that would send you to a website that looks like your bank's website, but is actually a fraudulent website, trying to extract personal information.

Hello DNS over HTTPS (DoH)

To prevent us from being tracked, and to secure out internet activity, we can implement DNS over HTTPS (DoH), which sends DNS queries/responses over HTTPS (encrypted).

Someone trying to snoop in on your DNS queries/responses after that is only going to see HTTPS data:

Encrypted DNS query/response

Traffic flow with DNS using DoH

Locally, your computer will send an unencrypted DNS request to the DNS server (Pi-hole), and Pi-hole, assuming it doesn't have an entry for that hostname already, will send an encrypted DNS (using DoH) query to our configured upstream DNS server (Cloudflare).

DNS queries will following this traffic pattern:

  • Client >>> local DNS server (Pi-hole), unencrypted DNS query, using port 53

  • Local DNS server (Pi-hole) >>> upstream DNS server (Cloudflare), encrypted DNS query, using port 443

  • Upstream DNS server (Cloudflare) >>> Local DNS server (Pi-hole), encrypted DNS response, using port 443

  • Local DNS server (Pi-hole), unencrypted DNS response, using port 53

Implement DoH for Pi-hole in Docker

This was my baseline Pi-hole, version 6, compose.yaml file:

services:
  pihole:
    container_name: pihole
    hostname: pihole-1
    # image: pihole/pihole:latest
    image: pihole/pihole:2025.07.1
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "67:67/udp"
      - "8880:80/tcp"
      - "8443:443/tcp"
    environment:
      TZ: 'America/Denver' #change to your local timezone
    networks:
      Internal:
        ipv4_address: 172.28.0.5
      net192:
        ipv4_address: 192.168.1.5
    volumes:
       - './etc-pihole/:/etc/pihole/'
       - './etc-dnsmasq.d/:/etc/dnsmasq.d/'
    dns:
      - 127.0.0.1
      - 1.1.1.1
    restart: unless-stopped

When initially deploying a new service, I specify the latest version, but after I get it working, I specify a specific tag, which is why you see pihole/pihole:latest and pihole/pihole:2025.07.01

Adding the cloudflared container and settings to the existing compose.yaml file for Pi-hole:

  cloudflared:
    # image: cloudflare/cloudflared:latest
    image: cloudflare/cloudflared:2025.8.1
    container_name: cloudflared
    network_mode: service:pihole
    command: proxy-dns
    environment:
      - "TUNNEL_DNS_UPSTREAM=https://1.1.1.1/dns-query,https://1.0.0.1/dns-query"
      - "TUNNEL_DNS_PORT=5053"
      - "TUNNEL_DNS_ADDRESS=0.0.0.0"
    restart: unless-stopped

Environment variables explained:

  • TUNNEL_DNS_UPSTREAM - What to use as the upstream DNS servers
  • TUNNEL_DNS_PORT - Which port for containerd to listen on
  • TUNNEL_DNS_ADDRESS - IP address to listen on, in this case, all addresses

The network_mode specifies to attach to the same network as the service, in this case, pihole.

Configure Pi-hole to use the cloudflared tunnel

Before adding the cloudflared container, Pi-hole was configured to use Cloudflare's public DNS servers, 1.1.1.1 and 1.0.0.1.

Once adding the cloudflared container, we need to configure Pi-hole to use DoH for the upstream DNS requests.

We can configure Pi-hole to use this either using a Docker environment variable or set it in the web UI.

Set upstream DNS server as a Docker environment variable

Under the pihole service definition, add the variable (in Pi-hole 6.x) FTLCONF_dns_upstreams:

FTLCONF_dns_upstreams: 127.0.0.1#5053

This configures Pi-hole to use the 127.0.0.1 server (localhost) and port 5053. As the cloudflared container runs on the same network as pihole, this works.

The major downside to using a Docker environment variable is that you can NOT change it in the web UI afterwords. You have to change it using the Docker environment variable, or remove it and add it using the web UI.

Set upstream DNS server in the Pi-hole web UI

Configure Pi-hole to use the local cloudflared service as the upstream DNS server by specifying 127.0.0.1#5053 as the Custom DNS (IPv4).

Settings >>> DNS, under Upstream DNS Servers, uncheck all DNS servers.

Under Custom DNS servers, add:

127.0.0.1#5053

Click Save & Apply at the bottom.

Configure Pi-Hole to use DoH

Test new settings

Using a browser, simply open https://1.1.1.1/help and look at the results.

If everything is configured correctly, you should see Yes for the section Using DNS over HTTPS (DoH):

Pi-Hole using DoH test

Complete compose.yaml file

For prosperity, this is my full compose.yaml file for Pi-hole and cloudflared:

services:
  pihole:
    container_name: pihole
    hostname: pihole-1
    image: pihole/pihole:2025.07.1
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8880:80/tcp"
      - "8443:443/tcp"
    environment:
      TZ: 'America/Denver' 
      # Variable for custom DNS server(s)
      FTLCONF_dns_upstreams: 127.0.0.1#5053
    networks:
      Internal:
        ipv4_address: 172.28.0.5
      net192:
        ipv4_address: 192.168.1.5
    volumes:
       - './etc-pihole/:/etc/pihole/'
       - './etc-dnsmasq.d/:/etc/dnsmasq.d/'
    dns:
      - 127.0.0.1
      - 1.1.1.1
    restart: unless-stopped

  cloudflared:
    image: cloudflare/cloudflared:2025.8.1
    container_name: cloudflared
    # Attach to the same network as the pihole service
    network_mode: service:pihole
    command: proxy-dns
    environment:
      - "TUNNEL_DNS_UPSTREAM=https://1.1.1.1/dns-query,https://1.0.0.1/dns-query"
      - "TUNNEL_DNS_PORT=5053"
      - "TUNNEL_DNS_ADDRESS=0.0.0.0"
    restart: unless-stopped

networks:
  Internal:
    external: true
  net192:
    external: true

References

Pi-hole documentation / Guides / DNS / cloudflared (DoH)
https://docs.pi-hole.net/guides/dns/cloudflared/

Github / cloudflare / cloudflared
https://github.com/cloudflare/cloudflared