← ~/content

Pi-hole v6 + Unbound on Proxmox — Network-Wide DNS (2026)

Tutorial~16 min read
Pi-hole v6 + Unbound on Proxmox — Network-Wide DNS (2026)
beginner~20 min

Prerequisites

  • Proxmox VE 9.x host
  • Router admin access (to change DHCP DNS settings)

Tools

  • SSH terminal
  • Web browser
  • Router admin panel

Software

  • pihole6.4
  • unbound1.22
  • proxmox-ve9.1
Watch on YouTube

Every device on your network makes hundreds of DNS queries a day. By default, those queries go to your ISP or a third-party provider like Google or Cloudflare — and they can see every domain you visit.

Pi-hole intercepts those queries at the network level, blocking ads, trackers, and telemetry domains before they ever leave your network. Pair it with Unbound, a recursive DNS resolver, and your queries go directly to the authoritative DNS servers — no third party in the middle.

This guide covers installing Pi-hole v6 and Unbound on a Proxmox LXC container. Pi-hole v6 is a major rewrite from v5 — the web interface, configuration system, and underlying architecture are all new. If you've followed older Pi-hole tutorials, most of them are outdated.

We'll also set up local DNS records, which are the foundation for the Caddy reverse proxy tutorial. Every service you deploy will get a friendly name like pbs.hake.rodeo instead of an IP address.

Prerequisites

  • A Proxmox VE 9.x host with SSH access
  • A static IP address available on your network for the Pi-hole container
  • Router admin access if you want all devices to use Pi-hole automatically via DHCP
  • A web browser for the Pi-hole admin interface

Step 1: Create the Container

Pi-hole needs a static IP because every device on the network will point to it for DNS. If the IP changes, DNS breaks for everything.

SSH into your Proxmox host and create the container:

On Proxmox host
pct create 100 local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
  --hostname pihole \
  --cores 1 \
  --memory 512 \
  --rootfs local-lvm:4 \
  --net0 name=eth0,bridge=vmbr0,ip=10.1.20.100/24,gw=10.1.20.1 \
  --nameserver 10.1.20.1 \
  --unprivileged 1 \
  --features nesting=1 \
  --onboot 1 \
  --start 1

WARNING

Replace the IP address, gateway, and DNS with values that match your network. The CT ID and hostname are your choice. Check your available templates with pveam list local if the template name doesn't match.

A few things to note about this command:

  • --nameserver 10.1.20.1 points DNS at the gateway temporarily. We can't point it at Pi-hole yet because Pi-hole isn't installed — the installer needs to download packages.
  • --features nesting=1 is required for Debian 13. Without it, systemd can't create namespaces and the container starts in a degraded state.
  • --onboot 1 enables autostart. Pi-hole is critical infrastructure — if your host reboots, DNS should come back automatically.
  • --start 1 starts the container immediately after creation.

Debian 13 has a quirk where /tmp is mounted as a tmpfs sized to the host's RAM, not the container's memory limit. On a host with 64 GB RAM and a container with 512 MB, this can cause apt to OOM-kill the container. Fix it before doing anything else.

Enter the container:

On Proxmox host
pct enter 100

Mask the tmpfs mount and set a root password (you'll need this for the PVE console later):

Inside CT 100
systemctl mask tmp.mount
Inside CT 100
passwd root

Exit the container and reboot to apply the tmpfs fix:

Inside CT 100
exit
On Proxmox host
pct reboot 100

Wait a few seconds for the container to come back up, then enter it again to finish the prep:

On Proxmox host
pct enter 100

Install curl, which the Pi-hole installer needs:

Inside CT 100
apt-get update && apt-get install -y curl
Inside CT 100
exit

Step 2: Install Pi-hole

Pi-hole's installer is interactive — it uses dialog prompts to walk you through each configuration choice. These dialogs require a proper terminal that pct enter via SSH doesn't always provide. Use the PVE web console for the install.

WARNING

The Pi-hole installer crashes with "cannot open tty-output" when run through pct enter over SSH. This is a known issue with how dialog handles TTY allocation in LXC containers. Use the PVE web console (CT 100 > Console) instead. After the install, you can switch back to your SSH client for everything else.

In the PVE web UI, click CT 100 > Console. Log in as root with the password you set in Step 1.

Download the installer first, then run it as a separate step. Piping directly to bash (curl | bash) can also cause TTY issues:

Inside CT 100 (PVE Console)
curl -sSL https://install.pi-hole.net -o /tmp/pihole-install.sh
Inside CT 100 (PVE Console)
bash /tmp/pihole-install.sh

The installer presents several screens:

Pi-hole Automated Installer welcome dialog

Click OK to proceed through the welcome and donation screens.

Pi-hole installer Static IP Needed dialog

Static IP confirmation — click Continue. We already configured a static IP when creating the container.

Pi-hole installer upstream DNS provider selection with Cloudflare selected

Upstream DNS provider — select any provider for now (e.g., Cloudflare). We'll replace this with Unbound in a later step.

Pi-hole installer Blocklists dialog with StevenBlack's Unified Hosts List

Blocklists — select Yes to include StevenBlack's Unified Hosts List. You can add more lists later.

Enable Logging — select Yes.

Pi-hole installer privacy mode selection with Show everything selected

Privacy mode — select 0 Show everything. This gives you full visibility in the query log.

The installer runs and finishes with a summary screen showing your admin password:

Pi-hole Installation Complete showing admin password and web interface URL

Save the admin password — you'll need it to log into the web interface.

NOTE

Pi-hole v6 replaced lighttpd and PHP with an embedded web server built directly into the pihole-FTL binary. If you've installed Pi-hole before, the installer may look different from what you remember — there's no separate web server to configure.

Step 3: Explore the Web UI

Open a browser and navigate to http://<your-pihole-ip>/admin. Log in with the password from the installer.

The first thing you'll notice is that Domains on Lists shows Error (-2). This is normal — the gravity database hasn't been populated yet.

Pi-hole v6 dashboard showing Error (-2) for Domains on Lists

Go to Tools > Update Gravity and click Update. This downloads the blocklists and populates the database. Without this, Pi-hole is running but not actually blocking anything.

Pi-hole v6's web interface is completely rebuilt. A few things to look at:

  • Dashboard — query stats, top blocked domains, query types, and upstream destinations
  • Query Log — real-time log of every DNS query hitting Pi-hole
  • Domains — manage allow and blocklists
  • Local DNS — where you'll add records for services like pbs.hake.rodeo
  • Settings — DNS configuration, privacy, API access, and more

TIP

The admin UI runs on port 80 — no port number needed in the URL. If you have another service using port 80 in the same container (you shouldn't with a dedicated Pi-hole LXC), FTL will fall back to port 8080.

Step 4: Install and Configure Unbound

Unbound is a recursive DNS resolver. Instead of forwarding your queries to Cloudflare or Google, it resolves them by going directly to the authoritative DNS servers — starting from the root servers and working down the chain. No third party ever sees your full query.

From here on, you can use your SSH client again — no more dialog prompts. Enter the container:

On Proxmox host
pct enter 100

Install Unbound from the Debian repos:

Inside CT 100
apt-get install -y unbound

NOTE

Unbound may show an error when it first installs — it tries to bind to port 53, which Pi-hole already owns. This is expected. We're about to configure it to use port 5335 instead.

Create the Unbound configuration file. This config is based on the official Pi-hole Unbound guide with settings tuned for a Pi-hole setup:

Inside CT 100
nano /etc/unbound/unbound.conf.d/pi-hole.conf

Paste the following, then save with Ctrl+X, Y, Enter:

/etc/unbound/unbound.conf.d/pi-hole.conf
server:
    verbosity: 0
 
    interface: 127.0.0.1
    port: 5335
 
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no
 
    prefer-ip6: no
 
    harden-glue: yes
    harden-dnssec-stripped: yes
 
    use-caps-for-id: no
 
    edns-buffer-size: 1232
 
    prefetch: yes
 
    num-threads: 1
 
    so-rcvbuf: 1m
    so-sndbuf: 1m
 
    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

The key settings:

  • port: 5335 — avoids the port 53 conflict with Pi-hole
  • harden-dnssec-stripped: yes — enables DNSSEC validation, rejecting responses with invalid signatures
  • prefetch: yes — refreshes popular domains before they expire from cache, keeping queries fast
  • private-address blocks — prevents DNS rebinding attacks by refusing to return private IP addresses from public queries

Now restart Unbound:

Inside CT 100
systemctl restart unbound

Check that it's running:

Inside CT 100
systemctl status unbound --no-pager | head -5

You should see active (running). If it fails, check the config for typos with unbound-checkconf.

WARNING

On some Debian installations, the unbound-resolvconf service can interfere with DNS configuration. If Unbound won't start after configuration, try: systemctl disable --now unbound-resolvconf.service

Step 5: Test Recursive DNS

Before connecting Unbound to Pi-hole, verify it works on its own. Install dig if it's not already available:

Inside CT 100
apt-get install -y dnsutils

Test a basic query through Unbound:

Inside CT 100
dig google.com @127.0.0.1 -p 5335

You should see a NOERROR status with an IP address in the answer section. The first query will be slow (200-500ms) because Unbound is resolving from root servers. That's recursive DNS working as designed — it's going to the root servers, then the TLD servers, then the authoritative server. Subsequent queries for the same domain are cached and near-instant.

Now test DNSSEC. This domain intentionally has a broken DNSSEC signature:

Inside CT 100
dig fail01.dnssec.works @127.0.0.1 -p 5335

This should return SERVFAIL — Unbound caught the invalid signature and refused to return the result. That's exactly what we want.

Test the positive case:

Inside CT 100
dig dnssec.works @127.0.0.1 -p 5335

This should return NOERROR. If you add the +ad flag, you'll see the ad (authenticated data) flag in the response, confirming DNSSEC validation passed.

Exit the container shell:

Inside CT 100
exit

Step 6: Connect Pi-hole to Unbound

Now that Unbound is working, point Pi-hole at it instead of Cloudflare.

In the Pi-hole web UI, go to Settings > DNS.

Under Upstream DNS Servers, uncheck all existing providers (Cloudflare, Google, etc.).

Pi-hole upstream DNS settings with Cloudflare checked — uncheck these

In the Custom DNS servers field, enter:

127.0.0.1#5335

The #5335 tells Pi-hole to use Unbound's port instead of the default 53.

Interface Settings

Scroll down to Interface settings. This controls which devices Pi-hole responds to.

Pi-hole interface settings showing Allow only local requests

The options are:

  • Allow only local requests — responds to queries from devices on the same subnet only (one hop away)
  • Respond only on interface eth0 — binds to eth0 specifically
  • Bind only to interface eth0 — stricter binding
  • Permit all origins — responds to queries from any source

If all your devices and containers are on the same subnet as Pi-hole, Allow only local requests is the right choice — it's the most restrictive option that works.

If you're querying Pi-hole from a different VLAN or subnet (e.g., your PC is on VLAN 10 but Pi-hole is on VLAN 20), you'll need Permit all origins. Since Pi-hole is internal-only with no internet exposure, there's no security risk either way.

Click Save.

Verify the Full Chain

Test that the complete chain works — client to Pi-hole to Unbound to root servers:

On Proxmox host
dig google.com @10.1.20.100

WARNING

Replace 10.1.20.100 with your Pi-hole's IP address.

You should get a NOERROR response. Check the Pi-hole query log in the web UI — the query should show as forwarded to 127.0.0.1#5335.

Pi-hole query log showing google.com forwarded to 127.0.0.1#5335 (Unbound)

That confirms the full chain: your device asks Pi-hole, Pi-hole asks Unbound, Unbound resolves recursively from the root servers.

Step 7: Local DNS Records

Local DNS records let you give services friendly names instead of IP addresses. Instead of accessing PBS at https://10.1.20.200:8007, you can use pbs.hake.rodeo:8007. This is also the foundation for the Caddy reverse proxy tutorial — Caddy handles the routing, but Pi-hole handles the name resolution.

NOTE

Without Caddy, local DNS only resolves the name to an IP — you'll still need the port number (e.g., pbs.hake.rodeo:8007). The Caddy tutorial eliminates the port requirement by routing everything through HTTPS on port 443.

In the Pi-hole web UI, go to Local DNS > DNS Records.

Add a record:

  • Domain: pbs.hake.rodeo
  • IP Address: 10.1.20.200

Click Add. The record appears immediately.

Pi-hole local DNS records showing pbs.hake.rodeo mapped to 10.1.20.200

Verify it resolves:

On Proxmox host
dig pbs.hake.rodeo @10.1.20.100

dig output showing pbs.hake.rodeo resolves to 10.1.20.200

You should see 10.1.20.200 in the answer section. Add more records for any other services you want to access by name — like uptime.hake.rodeo for Uptime Kuma.

Step 8: Point Your Network at Pi-hole

Now that Pi-hole is running with Unbound, update your containers and VMs to use it for DNS.

Update Containers

For LXC containers, pct set updates the DNS directly — no reboot needed:

On Proxmox host
pct set 100 --nameserver 127.0.0.1

Pi-hole's own container uses 127.0.0.1 since it queries itself. For other containers, use Pi-hole's IP:

On Proxmox host
pct set 102 --nameserver 10.1.20.100

Repeat for each container you want to use Pi-hole. Replace 102 with your container IDs.

Update VMs

VMs are different from containers — PVE doesn't control the VM's DNS directly. For VMs like PBS that were installed from their own ISO, you need to update DNS inside the VM.

SSH into the VM and edit /etc/resolv.conf:

On PBS VM
nano /etc/resolv.conf

Change the nameserver line to Pi-hole's IP, then save with Ctrl+X, Y, Enter:

/etc/resolv.conf
nameserver 10.1.20.100

This persists across reboots unless something like dhcpcd overwrites it.

NOTE

The qm set --nameserver command exists but only works if the VM has cloud-init configured. For VMs installed from a standalone ISO (like PBS), manual /etc/resolv.conf editing is the way to go.

Update Your PC or Router

To use Pi-hole from devices outside the Proxmox host, you can either update DNS per-device or update your router's DHCP settings.

Per-device (Windows example): Go to Settings > Network & Internet > your adapter > DNS settings > Edit. Set the Preferred DNS to Pi-hole's IP.

Windows 11 DNS settings with Preferred DNS set to Pi-hole

Router DHCP: Log into your router's admin panel, find the DHCP settings, and change the DNS server to Pi-hole's IP. All devices will pick it up on their next DHCP renewal.

Once DNS is pointing at Pi-hole, you can access services by name:

Browser showing pbs.hake.rodeo:8007 loading PBS via local DNS

TIP

If your PC is on a different VLAN than Pi-hole (e.g., your PC is on VLAN 10 but Pi-hole is on VLAN 20), you'll need to change Pi-hole's interface setting to Permit all origins. The default "Allow only local requests" only responds to devices on the same subnet.

Step 9: Add Monitoring and Backup

Uptime Kuma Monitors

Pi-hole is critical infrastructure — if it goes down, DNS breaks for every device using it. Set up three monitors in Uptime Kuma to catch different failure modes:

Ping monitor — is the container alive?

  • Type: Ping
  • Name: Pi-hole Ping
  • Hostname: your Pi-hole IP
  • Interval: 60 seconds

DNS monitor — is Pi-hole actually resolving queries?

  • Type: DNS
  • Name: Pi-hole DNS
  • Hostname: google.com
  • Resolver Server: your Pi-hole IP
  • Port: 53
  • Interval: 60 seconds

HTTP monitor — is the admin UI accessible?

  • Type: HTTP(s)
  • Name: Pi-hole HTTP
  • URL: http://<your-pihole-ip>/admin
  • Interval: 60 seconds

Each monitor catches a different problem:

  • Ping up, DNS down — container is running but Pi-hole crashed. SSH in and check systemctl status pihole-FTL.
  • Ping down — container itself is down. Start it from PVE.
  • DNS up, HTTP down — Pi-hole is resolving but the web UI is broken. Rare but possible.

Uptime Kuma showing all three Pi-hole monitors with Up status

PBS Backup

If you have Proxmox Backup Server set up, add the Pi-hole container to your backup job. In the PVE web UI, go to Datacenter > Backup, edit your backup job, and add CT 100 to the selection.

PVE backup job with CT 100 pihole added to the selection

A full container backup captures everything — the Pi-hole config at /etc/pihole/pihole.toml, the gravity database (blocklists), local DNS records, and query history.

Troubleshooting

Pi-hole Installer Crashes with "cannot open tty-output"

The dialog tool needs a proper TTY to render its interactive prompts. Running the installer through pct enter via SSH doesn't always provide one. Use the PVE web console (CT > Console) instead. Download the script first, then run it — don't pipe to bash.

Domains on Lists Shows Error (-2)

The gravity database hasn't been populated. Go to Tools > Update Gravity in the Pi-hole web UI, or run pihole -g from the CLI.

Unbound Fails to Start After Install

Unbound tries to bind to port 53 on first install, but Pi-hole already owns that port. Configure Unbound to use port 5335 (Step 4), then restart it. If it still fails, check for typos with unbound-checkconf and make sure unbound-resolvconf.service isn't interfering: systemctl disable --now unbound-resolvconf.service.

First Recursive DNS Query is Slow

Normal. The first query for any domain through Unbound takes 200-500ms because it resolves from root servers — walking the chain from root to TLD to authoritative. Subsequent queries for the same domain are cached and resolve in under 1ms.

"command not found" When Running pihole

The pihole binary is installed to /usr/local/bin/, which isn't always in the PATH. Use pct enter 100 for an interactive session where the full PATH is loaded.

Container OOM During apt install

Debian 13 mounts /tmp as tmpfs sized to the host's RAM, not the container's cgroup limit. If apt operations crash the container, mask the tmpfs mount: enter the container, run systemctl mask tmp.mount, exit, and pct reboot 100.

Queries Not Reaching Pi-hole from Other VLANs

If Pi-hole's interface setting is "Allow only local requests," it only responds to devices on the same subnet. Change it to Permit all origins in Settings > DNS > Interface settings. Since Pi-hole is internal-only with no internet exposure, this is safe.

DNS Breaks During Pi-hole Install

If the container's DNS is pointed at Pi-hole before Pi-hole is installed, the installer can't download packages. Keep DNS pointed at the gateway until Pi-hole is fully running, then switch.

IPv6 Installer Crash

If the Pi-hole installer crashes with "Unable to find IPv6 ULA/GUA address," disable IPv6 before running the installer: add net.ipv6.conf.all.disable_ipv6=1 and net.ipv6.conf.default.disable_ipv6=1 to /etc/sysctl.conf and run sysctl -p.

Summary

You now have network-wide DNS filtering and private recursive resolution:

  • Pi-hole v6 blocking ads, trackers, and telemetry at the DNS level
  • Unbound resolving queries directly from authoritative DNS servers — no third party in the chain
  • DNSSEC validation rejecting responses with invalid signatures
  • Local DNS records giving services friendly names like pbs.hake.rodeo
  • Three-layer monitoring via Uptime Kuma (ping, DNS, HTTP)
  • Automated backups via PBS

This is the DNS foundation for your homelab. In the Caddy tutorial, we'll add a reverse proxy that takes those local DNS records and serves every service over HTTPS with valid certificates — no ports to remember, no certificate warnings.

Related Products

Some links are affiliate links. I may earn a small commission at no extra cost to you.