Files
serverAppConfigs/serverAppConfigs-docs/Infrastructure/Pi-hole.md
T
Frank af5a1f69a2 docs(pi-hole): FTL can boot with the record file mounted and serve none of it
The 2026-10-19 reboot left every franksdank.cloud name NXDOMAIN for LAN clients while external names kept answering from cache; the file was mounted and readable and dns.hosts was intact, and 'pihole restartdns reload' restored every name in seconds. Mechanism not established. Instrument and remedy now on the owner page, plus a failure-mode row for the FTL rate-limit case seen beside it (10.0.2.x = a pasta container's forwarder).
2026-10-19 22:46:57 -07:00

7.5 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. After a Dankclaw restart every franksdank.cloud name went 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 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 boot-time load failure of the record sources, 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