Pi-hole with an Encrypted DNS-over-HTTPS Upstream
Pi-hole filters DNS requests for devices that use it as their resolver. This walkthrough runs Pi-hole and AdGuard’s dnsproxy in one Docker Compose project; Pi-hole sends upstream requests to dnsproxy, which forwards them to Cloudflare over DNS-over-HTTPS (DoH).
DoH encrypts the connection between dnsproxy and Cloudflare. The DNS provider can still see the queries, and observers can see the encrypted connection to that provider. This setup does not prevent every kind of tracking or replace endpoint security.
Use a server with a reserved LAN address. Replace 192.168.1.100 and 192.168.1.0/24 below with your server address and LAN subnet. Confirm host port 53 is available before starting the stack.
Step 1: Prepare a Private Admin Password
Create the Compose Directory and Password Secret
PreparationKeep the Pi-hole password in a local Compose secret file instead of writing it into the Compose document. Run these commands as the account that will run Docker Compose. The secret and local ignore file should stay out of version control.
mkdir -p "$HOME/pihole"cd "$HOME/pihole"umask 077openssl rand -base64 32 > pihole_webpasswdprintf '\npihole_webpasswd\n' >> .gitignore❯ View Expected Console Output
The password file is created with owner-only permissions. Store a separate copy of the generated password in your approved password manager if you need to sign in later.
Step 2: Define Pi-hole and the DoH Proxy
Configure the Two Services on One Private Docker Network
Docker ComposeSave this as compose.yaml. Both containers join the Compose project’s private network, so Pi-hole can reach the proxy by its service name. The DNS proxy is not published on a host port. Pi-hole’s DNS and admin ports bind only to the server’s LAN address; this requires that address to be assigned before Compose starts.
services: dnsproxy: image: adguard/dnsproxy:latest command: - -l - 0.0.0.0 - -p - "5053" - -u - https://cloudflare-dns.com/dns-query - -b - 1.1.1.1:53 restart: unless-stopped
pihole: container_name: pihole image: pihole/pihole:latest depends_on: - dnsproxy ports: - "192.168.1.100:53:53/tcp" - "192.168.1.100:53:53/udp" - "192.168.1.100:8080:80/tcp" environment: TZ: "Etc/UTC" WEBPASSWORD_FILE: pihole_webpasswd FTLCONF_dns_listeningMode: "ALL" FTLCONF_dns_upstreams: "dnsproxy#5053" volumes: - "./etc-pihole:/etc/pihole" secrets: - pihole_webpasswd restart: unless-stopped
secrets: pihole_webpasswd: file: ./pihole_webpasswd❯ View Expected Console Output
Pi-hole listens on the LAN address at DNS port 53 and the admin page at port 8080. dnsproxy listens on port 5053 inside the Compose network; Pi-hole uses that service name as its only upstream.
Step 3: Start the Stack and Verify DNS
Launch the Containers and Query Pi-hole
VerificationStart the services, check that both containers are running, then send a test query to the Pi-hole address. Open the admin page from a trusted LAN device at http://192.168.1.100:8080/admin and sign in with the password stored in the secret file.
cd "$HOME/pihole"docker compose up -ddocker compose psdig @192.168.1.100 example.com❯ View Expected Console Output
NAME IMAGE STATUS<project>-dnsproxy-1 adguard/dnsproxy:latest Up<project>-pihole-1 pihole/pihole:latest Up
;; ANSWER SECTION:example.com. <TTL> IN A <address returned by the upstream>Step 4: Set Pi-hole as the LAN DHCP DNS Server
Update the Router's LAN DHCP DNS Option
Client SetupIn the router’s LAN DHCP settings, set the DNS server handed to clients to 192.168.1.100, then renew a test client’s DHCP lease. Keep the router’s WAN or upstream DNS setting separate; pointing Pi-hole’s upstream back to Pi-hole can create a loop. If your network advertises IPv6 DNS through router advertisements, configure Pi-hole for IPv6 too or clients may continue bypassing it.
ipconfig /releaseipconfig /renewipconfig /flushdnsnslookup example.com 192.168.1.100❯ View Expected Console Output
Server: pi.holeAddress: 192.168.1.100
Name: example.comAddresses: ...