I didn't have time to write a proper tutorial but I've set up two synced Pi-hole LXCs this week so I thought I might give a short quick-and-dirty tutorial as long as I remeber everything.
I'm using the "debian-12-standard_12.2-1_amd64.tar.zst" as the template on two PVE nodes version 8.1.4. Put one LXC on each PVE node so one Pi-hole is always working even if a node goes down. You don't need a PVE cluster or high availability for this. If you don't got two PVE nodes you could also use one PVE node + 1 Raspi but then you might need to customize some stuff.
The basic idea is like this:
You have your router that acts as your single client-facing DNS server and DHCP server. Clients in your network will use this router as the gateway and single DNS server. I use two OPNsense VMs on two different PVE nodes in HA configuration using virtual IPs. So there is always a router running even if a node goes down. This isn't part of this tutorial but you can get an idea by looking at this:
https://www.thomas-krenn.com/en/wiki/OPNsense_HA_Cluster_configuration
The router's DNS server will resolve the hostnames of your DHCP clients and forward DNS requests to the Pi-hole LXCs. There are two Pi-hole LXCs on two different PVE nodes. These will be kept in sync using gravity-sync so you can edit the config of any of those Pi-holes and it will be synced to the other one as well. What won't be synced are statistics and logs. So it would be good if all clients would only use a single master pi-hole. And only if that master Pi-hole isn't available they will use the second backup Pi-hole. That way 99,9% of the time only a single Pi-hole is used that got nearly all of the logs/statistics. Also useful as not all OSs will prioritize a primary and secondary DNS server and will choose any of those two at random.
To accomplish this we make use of keepalived which allows us to define a master LXC and a backup LXC. Once the master LXC fails the backup LXC becomes the new master. When the old master becomes available again this reverts back to normal. Each LXC got a static IP you can use to access the LXC and its Pi-hole's webUI. But there is also a third IP, a Virtual IP (or short VIP), that both LXCs are sharing. This VIP will always point to the Pi-hole that is currently the master. The router's DNS server is only forwarding requests to this VIP.
The Pi-hole of the master LXC then receives these requests, filters them and forwards them again. Usually, you would point Pi-hole to some public DNS servers like 1.1.1.1, 8.8.8.8 and so on. I don't like that for privacy and anti-censorship reasons and I want to resolve domains on my own using recursive DNS lookups. For that, each LXC is also running an unbound DNS server that is only listening on localhost. Pi-hole then forwards DNS requests, that didn't get filtered, to this local unbound DNS server and this will do the recursive DNS lookups.
Here the steps:
1.) Create two LXC
Create two new unprivileged LXCs using the latest “Debian 12 Standard” template. I will call one "PiholeMaster" and the other one "PiholeBackup".
1 core, 256MiB RAM, 128MiB swap and 8GiB (4GiB should work too) root filesystem will be fine. Single NIC eth0 using an IP that is free, not part of your DHCP server IP range and part of router's subnet. I will refer to the IP of the "PiholeMaster" LXC as <MasterIP> and for the "PiholeBackup" LXC as <BackupIP>. Use the IP of your router as the gateway. For the DNS server, you might want to choose a public one (like "1.1.1.1") so your Pi-hole LXCs should be able to go online even if something is broken and DNS resolving by Pi-hole/Unbound isn't working. The virtual IP isn't defined in the PVE webUI and I will refer to it as <VirtualIP>. This virtual IP should also be a free IP of your router's subnet that isn't part of the DHCP range.
2.) Doing an initial upgrade
For both LXCs do:
- connect via SSH
- upgrade all packages:
apt update && apt full-upgrade
The following steps are optional and not needed but I do them for each Debian 12 LXC I set up.
3.) Optional: Disable IPv6 and reduce swappiness
I'm not using IPv6 anywhere and therefore don't want the LXC to use it, so I'm disabling it. And I want to minimize swapping to reduce wear on the SSDs. For that run:
Code:
echo "# disable IPv6" > /etc/sysctl.d/sysctl.conf
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.d/sysctl.conf
echo "# only swap to prevent OOM" >> /etc/sysctl.d/sysctl.conf
echo "vm.swappiness = 0" >> /etc/sysctl.d/sysctl.conf
4.) Optional: Enable unattended upgrades
In my opinion it is a good idea to install security patches as soon as possible. It's not likely that these will break anything as feature upgrades aren't automatically installed. But make sure to always have recent automated backups in case something goes wrong. For that run:
apt-get update && apt-get install unattended-upgrades apt-listchanges
5.) Optional: Add non-free repos
The Debian LXC ships with repos that allow to install open source software only. To be able to install proprietary software add “non-free non-free-firmware” to all 3 default repos. For that run:
Code:
sed -i -E 's/deb http:\/\/deb.debian.org\/debian bookworm main contrib$/deb http:\/\/deb.debian.org\/debian bookworm main contrib non-free non-free-firmware/g' /etc/apt/sources.list
sed -i -E 's/deb http:\/\/deb.debian.org\/debian bookworm-updates main contrib$/deb http:\/\/deb.debian.org\/debian bookworm-updates main contrib non-free non-free-firmware/g' /etc/apt/sources.list
sed -i -E 's/deb http:\/\/security.debian.org bookworm-security main contrib$/deb http:\/\/security.debian.org bookworm-security main contrib non-free non-free-firmware/g' /etc/apt/sources.list
6.) Optional: Move /tmp to ramdisk
Move /tmp to ramdisk to reduce wear on SSDs:
Code:
cp /usr/share/systemd/tmp.mount /etc/systemd/system/
systemctl enable tmp.mount
7.) Optional: Set timezone
- set timezone to Europe/Berlin:
timedatectl set-timezone Europe/Berlin
- if you want an UI for selecting your timezone run this instead:
dpkg-reconfigure tzdata
8.) Optional: Setup locales
- setup your countrys locales by running:
dpkg-reconfigure locales
9.) Optional: Limit logging
I personally don't want to write logs to disk as I use a log collector that forwards them to centralized log server with a persistent database anyway. So I want journald to only store 20MB of logs in volatile RAM to reduce IO and SSD wear. But keep in mind that in case you do this, you will lose the journal once you shutdown the LXC. To do that:
Code:
mkdir /etc/systemd/journald.conf.d
echo "[Journal]" > /etc/systemd/journald.conf.d/journald.conf
echo "# only write logs to volatile RAM and limit it to 20MB" >> /etc/systemd/journald.conf.d/journald.conf
echo "Storage=volatile" >> /etc/systemd/journald.conf.d/journald.conf
echo "SystemMaxUse=20M" >> /etc/systemd/journald.conf.d/journald.conf
echo "RuntimeMaxUse=20M" >> /etc/systemd/journald.conf.d/journald.conf
10.) Installing Pi-hole
For both LXCs do:
- install Pi-hole via install script:
Code:
apt update && apt install curl
curl -sSL https://install.pi-hole.net | PIHOLE_SKIP_OS_CHECK=true bash
(this PIHOLE_SKIP_OS_CHECK=true is needed when working with Pi-hole in an LXC as otherwise it is failing the OS checks as it doesn'T recognize a Debian 12 LXC as a normal Debian 12 distro)
- choose “eth0” as the interface
- select any upstream DNS provider (I've chosen DNS.Watch)
- continue with preselected blocklists
- install web admin interface
- install webserver
- choose to log queries with show everything (you can disable that later but useful for checking if everything works)
- write down the webUI password at the bottom of the “Installation complete!” dialog!!!
- open "http://<MasterIP>/admin" and "http://<BackupIP>/admin", login and check if everything is working
11.) Optional: Daily Pi-Hole Autoupdate
Do the following for both LXCs in case you want Pi-hole to be auto updated every day:
- create systemd service for it:
nano /etc/systemd/system/update_pihole.service
- Add there:
Code:
[Unit]
Description=Update Pi-hole
After=local-fs.target remote-fs.target network-online.target
StartLimitIntervalSec=0
[Service]
Type=oneshot
User=root
ExecStart=/bin/bash -c "PIHOLE_SKIP_OS_CHECK=true sudo -E pihole -up"
[Install]
WantedBy=multi-user.target
- create timer:
nano /etc/systemd/system/update_pihole.timer
- Add there:
Code:
[Unit]
Description=Daily update of Pi-hole
After=local-fs.target remote-fs.target network-online.target
[Timer]
Unit=update_pihole.service
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
- enable and start timer:
Code:
systemctl enable update_pihole.timer
systemctl start update_pihole.timer
- reload services/timers:
systemctl daemon-reload