--- type: reference description: "Pi-hole is the LAN DNS server for the homelab." tags: - infrastructure - pi-hole timestamp: 2026-10-19 --- # Pi-hole Pi-hole is the LAN DNS server for the homelab. It runs as a quadlet container on Dankclaw (192.168.0.143). It answers the flat `franksdank.cloud` / `franksdank.casa` service names and filters ad traffic on the LAN. It retired the dnsmasq quadlet on 2026-08-24. See [[LAN-DNS]] for the name-resolution flow and the router hand-off. ## Components | Component | Where | Role | |-----------|-------|------| | `pihole` quadlet | `dankclaw/pihole/quadlet/pihole.container` | Boot-persistent container unit | | FTL | `docker.io/pihole/pihole:latest` | DNS engine (a dnsmasq fork) + ad blocking | | `addn-hosts` | `dankclaw/pihole/addn-hosts` | One A record per Caddy vhost | | sysctl drop-in | `dankclaw/pihole/sysctl/99-dnsmasq.conf` (repo) → `/etc/sysctl.d/` | Rootless port-53 threshold, applied at boot | | env file | `/home/dank/Containers/pihole/env` (host) | `ServerIP`, `TZ`, `DNS1`, `DNS2`, `WEBPASSWORD` | | Web UI | `pihole.franksdank.cloud` (Caddy → 8083) | Dashboard + admin, family login | ## Quadlet - **Install path on Dankclaw:** `~/.config/containers/systemd/pihole.container` (unit `pihole.service`) - **Network:** `slirp4netns` (the same pattern as open-webui and gitea) - **Published ports:** 53/udp + 53/tcp (DNS), 8083/tcp (web UI) - **Mounts:** - `/home/dank/Containers/pihole/etc-pihole` → `/etc/pihole` (config + state, preserved across restarts) - `/home/dank/Containers/pihole/etc-dockers` → `/etc/dockers` (blocklists) - `/home/dank/Containers/pihole/addn-hosts` → `/etc/pihole/hosts/addn-hosts` (read-only; the record file from this repo) - **Restart:** `Restart=always`, `RestartSec=5` - **Deploy:** `./scripts/deploy-pihole-quadlet.sh dankclaw` (idempotent; preserves the data dirs) ## Records FTL does not read `/etc/dnsmasq.d/` — its generated `dnsmasq.conf` has no `conf-dir`. The only file-based local-record mechanism is `hostsdir=/etc/pihole/hosts`: FTL parses every file in that directory as an `/etc/hosts` file. The quadlet therefore mounts the repo record file there, read-only: ``` /home/dank/Containers/pihole/addn-hosts -> /etc/pihole/hosts/addn-hosts ``` The file format is hosts-style (` [ ...]`), one A record per Caddy vhost, all pointing at `192.168.0.143`. The file stays in sync with the Caddyfile (`tests/pihole.bats` cross-checks the two). Add a name there and in Porkbun, then deploy. See [[LAN-DNS]] for the two-place rule. **Pitfall (2026-08-24):** the first quadlet mounted the file into `/etc/dnsmasq.d/`. FTL never read it; names only resolved from the query cache. A fresh name returned NXDOMAIN. Mount into `/etc/pihole/hosts/`, not `/etc/dnsmasq.d/`. **Pitfall (2026-10-19): FTL can come up with the record file mounted and still serve none of it.** A plain `systemctl --user restart pihole` (16:25, inside the open-notebook deploy window) left every `franksdank.cloud` name NXDOMAIN for LAN clients while **external names kept answering** — from FTL's cache, which is exactly what makes the failure read like a client or an upstream problem. **The trigger is a container restart, not a boot**, and that matters for where the check belongs: `uptime -s` reads 2026-10-18 01:24 (two days of uptime) while `journalctl --user -u pihole.service` shows `Stopping pihole.service` at 16:25:23 and the container's `StartedAt` at 16:25:27 — so verify after **any** pihole restart, deploy-triggered or manual, not only after a reboot. The instrument is on the box: `getent hosts grafana.franksdank.cloud` (or `dig +short @192.168.0.143 `) answering empty means FTL is holding no local records at all; here the file was present and readable inside the container (`-rw-rw-r-- … /etc/pihole/hosts/addn-hosts`, 44 records) and `dns.hosts` in `pihole.toml` was intact. `podman exec pihole pihole restartdns reload` had every name answering within seconds. The mechanism is **not established** — the same mounts and the same config read fine before and after the reload — so treat it as a load failure of the record sources at FTL start, not as a lost or unmounted file. What it cost: the whole Playwright gate, 18 specs across every vhost failing `ERR_NAME_NOT_RESOLVED` while the fleet itself was healthy ([[E2E Suite]]). ## Upstream The slirp netns has no useful `resolv.conf`. The env file pins the upstreams: `DNS1=1.1.1.1`, `DNS2=1.0.0.1` (mirrors the retired `dnsmasq.conf` `server=` lines). Every query that is not a local record goes to those resolvers. The zone apex and unknown subdomains fall through to Porkbun. ## Web UI and API - **URL:** `https://pihole.franksdank.cloud/` (Caddy vhost, family basicauth → 8083) - **Password:** `WEBPASSWORD` in the env file (the shared family password, the same credential as `CADDY_BASICAUTH_HASH`) - **API:** `https://pihole.franksdank.cloud/api/...` — authenticate with the session cookie, then query the blocklist, the local DNS records, and the query log - **Port on the LAN:** `http://192.168.0.143:8083/` (direct, no Caddy) ## Host prerequisites 1. `net.ipv4.ip_unprivileged_port_start=53`, persisted in `/etc/sysctl.d/99-dnsmasq.conf` — rootless Podman may then publish port 53. The repo file `dankclaw/pihole/sysctl/99-dnsmasq.conf` is the single source of truth: `deploy_pihole` installs it, stages any drop-in that sets a value above 53 in the home trash, comments out a stale active line in `/etc/sysctl.conf`, and applies everything with `sysctl --system` (the same code path boot uses via systemd-sysctl). **Incident (2026-08-26):** the value was reset to 80 by *two* sources — a later-sorted drop-in (`99-rootless-ports.conf`) and a stale line in `/etc/sysctl.conf`, which systemd-sysctl processes **after** all of `sysctl.d/` and therefore overrides every drop-in. The runtime override from an earlier manual `sysctl -w` died with a reboot, and `pihole.service` crash-looped on `bind: permission denied` for port 53 (restart counter 1645) until both conflicts were removed. 2. `DNSStubListener=no` in `/etc/systemd/resolved.conf` — the resolved stub would otherwise hold port 53 inside the container netns. Both prerequisites survive the dnsmasq → Pi-hole swap. Do not remove them. ## Failure modes | Symptom | First check | |---------|-------------| | `dig @192.168.0.143` fails entirely | Quadlet down: `systemctl --user status pihole` | | A name does not resolve for a new client | The client's `resolv.conf` does not show `192.168.0.143` — the G36 hand-off is missing. See [[LAN-DNS]] | | The web UI asks for a password loop | `WEBPASSWORD` in the env file is the placeholder `changeme` — fill the real value, restart the unit | | Upstream queries fail (local names still answer) | `DNS1`/`DNS2` in the env file are missing or wrong | | Port 53 `bind: permission denied` crash loop after a reboot | sysctl conflict: the value must be 53 at boot, and remember `/etc/sysctl.conf` is applied **after** all of `sysctl.d/`. Redeploy pihole — `deploy_pihole` reinstalls the drop-in, neutralizes conflicting drop-ins, and comments out a stale `/etc/sysctl.conf` line | | Single-label name (for example `dankclaw`) answers `127.0.1.1` | The rootless container `/etc/hosts` carries the host entry (`127.0.1.1 `), and FTL serves it. Use the FQDN, or add an `/etc/hosts` entry on the client | | Every `franksdank.cloud` name is NXDOMAIN for LAN clients while external names still answer | FTL holds no local records: `getent hosts ` on Dankclaw comes back empty → `podman exec pihole pihole restartdns reload` (the 2026-10-19 pitfall under **Records**) | | One client gets nothing while every other client is fine | FTL rate-limiting that client: `podman exec pihole grep Rate-limiting /var/log/pihole/FTL.log`. A `10.0.2.x` client address is a pasta container's DNS forwarder, not a machine | ## See Also - [[LAN-DNS]] — name resolution flow, router hand-off, split DNS - [[Caddy]] — the `pihole.franksdank.cloud` vhost - [[Dankclaw Stack]] — the host that carries Pi-hole