Files
Frank e914ecbca6 docs(pi-hole): the record-load trap rides a container restart, not a boot
The 2026-10-19 write-up inferred a host restart from FTL's 16:25 start line. It was wrong: uptime -s reads 2026-10-18 01:24 (two days up) and the user journal shows 'Stopping pihole.service' at 16:25:23 with StartedAt 16:25:27 — a plain container restart inside the open-notebook deploy window. That moves where the verification belongs: after every pihole restart, not just after a reboot.
2026-10-20 10:56:39 -07:00

7.9 KiB

type, description, tags, timestamp
type description tags timestamp
reference Pi-hole is the LAN DNS server for the homelab.
infrastructure
pi-hole
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 (<IP> <name> [<name> ...]), 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 <name>) 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 <host>), 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 <name> 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