Block Every Ad and Tracker on Your Entire Network: Self-Hosting Pi-hole via Docker on Proxmox

🎧 Listen to the 60-Second Audio Recap:

The average news page loads trackers from over 70 third-party domains before you read a single word. Every one of those requests tells an advertising network your approximate location, your device type, your browser, and what you were reading five minutes ago. This happens silently, automatically, and before the page finishes rendering.

Pi-hole ad blocking stops this at the network level. It runs as a DNS server on your own hardware. When any device on your network asks “where is doubleclick.net?”, Pi-hole answers with 0.0.0.0 — a dead end. The ad never loads. The tracker never phones home. Because it operates at the DNS layer, it covers every device on your network: your smart TV, your phone, your partner’s laptop, your IoT thermostat. No per-device installation. No browser extension to maintain.

This guide deploys Pi-hole v6 as a Docker container on a Proxmox LXC. If you are running bare metal or a dedicated x86 machine, the Docker Compose file is identical — only the host setup differs. We cover two routes: a zero-risk single-device test (Newbie Nora), and a full network-wide production deployment (Pro Paul).

Prerequisites and Minimal Hardware

What You Need Before Starting

Hardware minimums:

  • Any Proxmox node or a mini PC, NUC, or repurposed x86 machine works fine.
  • An LXC container or VM with at least 512 MB RAM.

Software prerequisites:

  • Docker and Docker Compose installed, with Portainer running and accessible. If you have not done this yet, follow our Docker + Portainer on Proxmox LXC guide first — it is the foundation for everything in this guide.

Network prerequisites (these are critical):

  • A static IP address assigned to the LXC or VM. Your entire network will point its DNS here. A changing IP breaks everything.
  • Port 53 (TCP and UDP) must be free on the host. This is the single most common deployment failure point.
  • Port 80 (TCP) must be free on the host for the admin interface.
  • Admin access to your router’s DHCP and DNS settings.

Quick pre-flight checklist:

  • [ ] Docker and Portainer installed and accessible
  • [ ] Static IP confirmed on the LXC host
  • [ ] Router admin credentials available
  • [ ] Ports 53 and 80 confirmed not in use

A Critical Note on Pi-hole v6 — Breaking Changes from v5

Pi-hole v6 introduced a new environment variable naming convention using the FTLCONF_ prefix. The old WEBPASSWORD variable is deprecated and non-functional in v6.

Warning: If you find older tutorials using WEBPASSWORD=, they will not work with Pi-hole v6. The correct variable is FTLCONF_webserver_api_password. Using the old variable will deploy a container you cannot log into.

Method 1 — The Quick Start (Newbie Nora Route)

Goal: Test Pi-hole on One Device Before Going Network-Wide

This method carries zero risk. You make no router changes. If you do not like the result, you undo one DNS setting on your laptop and Pi-hole is completely out of the picture. This is ideal for first-time self-hosters, renters without router access, or anyone who wants to verify the setup works before committing.

Step 1 — Deploy the Pi-hole Container via Portainer

  1. Open Portainer in your browser.
  2. Navigate to Stacks in the left sidebar.
  3. Click Add Stack.
  4. Give the stack a name — for example, pihole.
  5. Paste the following compose file into the Web Editor:
services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    restart: unless-stopped
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      TZ: "Europe/Amsterdam"
      FTLCONF_webserver_api_password: "ChooseAStrongPassword123!"
      FTLCONF_dns_listeningMode: "all"
    volumes:
      - /mnt/snelle_data/App_Data/pihole/etc-pihole:/etc/pihole
    cap_add:
      - NET_ADMIN

Adjust the volume path to match your own storage mount. Change the timezone to your own region. Replace the password with something strong and unique.

Click Deploy the Stack. Portainer will pull the image and start the container. If you see a port 53 error during deployment, jump directly to the Troubleshooting section below — this is the systemd-resolved conflict and it has a clean fix.

Step 2 — Point Only Your Laptop’s DNS to Pi-hole

Do not touch your router yet. Instead, manually set the DNS server on just one device to the static IP of your Pi-hole host.

Windows: Settings → Network and Internet → Change Adapter Options → Right-click your adapter → Properties → Internet Protocol Version 4 (TCP/IPv4) → Properties → Use the following DNS server → enter <pihole-ip>.

macOS: System Settings → Network → select your connection → Details → DNS tab → click the + button → enter <pihole-ip>.

Linux:

sudo resolvectl dns eth0 <pihole-ip>

Replace eth0 with your actual interface name. Run ip link to find it.

Step 3 — Verify It Works on Your Single Device

Open a browser and visit a news site known for heavy advertising. You should see noticeably fewer banner ads and no tracking pixels loading.

For a definitive technical check, run this from your terminal:

dig @<pihole-ip> doubleclick.net
# Expected result: 0.0.0.0 in the ANSWER section (blocked)

If you see 0.0.0.0, Pi-hole is intercepting and blocking correctly. If you are satisfied, proceed to Method 2 to roll this out to your entire network.

Method 2 — The Full Production Setup (Pro Paul Route)

Step 1 — Clear Port 53 (The Number One Deployment Pitfall)

On Debian and Ubuntu systems — which includes most Proxmox LXC templates — systemd-resolved runs a stub DNS listener on port 53 by default. When Docker tries to bind the Pi-hole container to port 53, it finds the port already occupied and throws a bind: address already in use error.

The fix is to disable the stub listener and provide a direct fallback nameserver. Run these four commands in sequence on your LXC host:

sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
sudo systemctl restart systemd-resolved

Do not skip the resolv.conf replacement step. When you remove the symlink, the host loses its DNS reference. Without the fallback nameserver line pointing to 1.1.1.1, the host itself will lose DNS resolution immediately. Write the fallback before restarting the service.

Verify port 53 is now free:

sudo ss -tulpn | grep :53
# Expected: no output

If the command returns no output, the port is clear and you can proceed.

Step 2 — Deploy the Full Production Stack via Portainer

Use the same docker-compose.yml shown in Method 1, Step 1. Deploy it via Portainer Stacks exactly as described there.

A few production-specific notes:

  • The volume path /mnt/snelle_data/App_Data/pihole/etc-pihole is specific to our lab setup. Adjust it to your own storage mount. The directory will be created automatically on first run.
  • cap_add: NET_ADMIN is required. Even if you do not use Pi-hole’s built-in DHCP server, this capability is needed for certain network operations the FTL process performs.
  • restart: unless-stopped ensures the container comes back up automatically after a Proxmox host reboot.

Pi-hole v6 config note: FTLCONF_dns_listeningMode: "all" is required when running Pi-hole behind Docker NAT. Without it, the container only listens on its internal Docker interface and will not accept queries from devices on your physical network.

Step 3 — Access the Admin Interface and Add Blocklists

Navigate to http://<pihole-ip>/admin in your browser. Log in using the password you set in FTLCONF_webserver_api_password.

Pi-hole v6 ships with the StevenBlack Unified Hosts list active by default. This covers the most common ad and malware domains and is a solid starting point. We recommend adding at least one additional list for broader coverage:

List Name URL Focus
OISD Full https://big.oisd.nl Broad ad and tracker blocking
HaGeZi Multi Pro https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt Aggressive multi-category blocking

To add a list: navigate to Lists in the sidebar → click Add → paste the URL → click Save. Then go to Tools → Update Gravity to download and apply the new lists.

Step 4 — Activate Network-Wide Blocking via Your Router

  1. Log into your router’s admin panel.
  2. Navigate to the DHCP settings section.
  3. Find the DNS Server field and set the Primary DNS to <pihole-ip>.
  4. Save the settings.

Critical: Do not set a secondary or fallback public DNS server (such as 8.8.8.8) alongside Pi-hole. Devices will randomly use the secondary when the primary appears slow, silently bypassing Pi-hole entirely. The only acceptable secondary DNS is a second Pi-hole instance. We cover that in the limitations section below.

After saving, force a DHCP lease renewal on your devices so they pick up the new DNS setting:

# Linux and macOS
sudo dhclient -r && sudo dhclient

# Windows
ipconfig /release && ipconfig /renew

Alternatively, toggle Wi-Fi off and on, or simply reboot the device.

Configuration and Validation — Confirming Everything Works

Dashboard Check

Wait two to five minutes and let your devices generate normal traffic. Then open http://<pihole-ip>/admin.

You should see the live query stream populating with domain requests from devices on your network. The Queries Blocked percentage should be climbing. A healthy home network typically lands between 5% and 15% blocked queries. Higher is not always better — it can indicate over-blocking. Lower may mean devices are bypassing Pi-hole.

DNS Resolution Check via dig

# Test 1: A legitimate domain must resolve correctly
dig @<pihole-ip> google.com
# Expected: A valid IP address in the ANSWER section

# Test 2: A known ad domain must be blocked
dig @<pihole-ip> doubleclick.net
# Expected: 0.0.0.0 in the ANSWER section

If Test 1 returns a valid IP and Test 2 returns 0.0.0.0, your Pi-hole is working correctly.

Real-World Device Check

Open an ad-heavy news site on a phone connected to your Wi-Fi. You should see significantly fewer or zero banner ads compared to before.

If ads still appear on the phone, the most likely cause is that the phone has not yet received the updated DNS via DHCP. Toggle Wi-Fi off and on, or forget and rejoin the network. Then recheck the Pi-hole dashboard — you should see the phone’s queries appearing in the live stream.

The Honest Truth — Limitations and Caveats

Pi-hole Is a Single Point of Failure

This is the most important limitation to understand before you go network-wide. If the Pi-hole container crashes, or if your Proxmox host goes down, every device on your network loses DNS resolution. No DNS means no internet, from the user’s perspective.

Mitigations:

  • Run your Proxmox host on a UPS to survive power outages.
  • Deploy a second Pi-hole instance on a different host and set it as the secondary DNS in your router. This is the only acceptable use of a secondary DNS entry here — a second Pi-hole, not a public resolver.
  • Document a manual recovery procedure: how to quickly set your router DNS back to 1.1.1.1 if Pi-hole goes down unexpectedly.

The Admin Interface Is HTTP-Only by Default

Port 80 is unencrypted. Do not expose it to the internet. For internal use on a trusted home network, this is acceptable. If you want HTTPS access, the correct path is to place Pi-hole behind a reverse proxy such as Nginx Proxy Manager or Traefik.

What Pi-hole Ad Blocking Cannot Do

Pi-hole is not magic. Be aware of these specific gaps:

  • YouTube ads are served from the same domains as YouTube content. Blocking those domains breaks YouTube entirely. Pi-hole cannot block YouTube ads without collateral damage.
  • First-party ads — such as ads served directly from facebook.com — cannot be blocked via DNS without breaking the service itself.
  • DNS-over-HTTPS (DoH) bypass: Some Android apps and browsers have hardcoded DoH resolvers (Google, Cloudflare). These bypass Pi-hole entirely by encrypting DNS queries to a specific server. You can block known DoH endpoints at the firewall level, but this is an arms race.

Pi-hole is not a replacement for a browser-level adblocker like uBlock Origin. Use both for maximum coverage. Pi-hole handles network-level blocking across all devices. uBlock Origin handles the gaps that DNS blocking cannot reach.

If you are building a privacy-first home network, Pi-hole pairs well with other local-control projects. For example, once you have your DNS layer locked down, you might want to look at flashing OpenWrt on your router to eliminate cloud dependencies at the network hardware level as well. And to keep that same ad-blocking when you are away from home, route a self-hosted WireGuard VPN through your Pi-hole.

Troubleshooting Common Errors

Error: bind: address already in use on Port 53

Cause: systemd-resolved stub listener is still active and holding port 53.

Fix: Run the four-command sequence from Method 2, Step 1. Then verify with:

sudo ss -tulpn | grep :53
# Must return no output

If output still appears, confirm the /etc/systemd/resolved.conf edit was applied correctly and that you restarted the service. In some cases a full host reboot is cleaner than a service restart.

Error: Entire Network Loses Internet After Router DNS Change

Cause: The Pi-hole container is not running, or the IP address entered in the router is incorrect.

Fix sequence:

  1. Open Portainer and confirm the Pi-hole container shows status running and is healthy.
  2. Confirm the static IP on the host:
ip addr show

Verify the IP matches what you entered in the router. If there is a mismatch, correct the router setting. For immediate network recovery while you diagnose, set the router DNS temporarily back to 1.1.1.1.

Error: Cannot Log Into Admin Interface — Password Not Working

Cause: Password mismatch, or the container was deployed with the old WEBPASSWORD variable from a v5-era tutorial, which does nothing in v6.

Fix: Reset the password directly via the container console:

docker exec -it pihole pihole setpassword YourNewPassword

Then log in at http://<pihole-ip>/admin with the new password.

Error: Queries Not Appearing in Dashboard From Network Devices

Cause: Devices are still using their old cached DNS from a previous DHCP lease. The router change has not propagated yet.

Fix: Force a DHCP lease renewal on the affected devices using the commands from Method 2, Step 4. Confirm the router actually saved the DNS change — some routers require a reboot to apply DHCP option changes.

Conclusion and Next Steps

What You Have Built — Final Checklist

  • [x] Pi-hole v6 running as a persistent Docker container with a static IP
  • [x] Port 53 conflict with systemd-resolved resolved cleanly
  • [x] Router DHCP pointing all network devices to Pi-hole as their DNS server
  • [x] Blocking verified via the admin dashboard and dig commands
  • [x] Limitations understood: single point of failure, HTTP-only admin, DNS blocking gaps

Next Step — Add Unbound for True DNS Privacy

Your current setup is a significant improvement over the default. But there is one remaining gap. When Pi-hole encounters a domain it does not block, it forwards that query to an upstream resolver — typically Cloudflare’s 1.1.1.1 or Google’s 8.8.8.8. Those resolvers still see every domain your network queries. They just do not serve you ads directly.

The next level is Unbound — a local recursive DNS resolver. Instead of forwarding queries to a third party, Unbound contacts the root DNS servers directly and resolves domains itself. No third party sees your query patterns. This is the setup the r/pihole community consistently recommends as the gold standard for home DNS privacy.

We cover the full Pi-hole plus Unbound deployment in the next guide in this series.