kadet-hub logokadet-hub

Pi-hole on Kubernetes without Docker Compose, with Kapitan

Pi-hole, a DNS server that blocks ads and trackers for every device of a network, rendered by the kadet-hub kubernetes generator 1.14.0 from the Docker Compose example in the README of docker-pi-hole 2026.10.1.

What it deploys

Required parameters

applications.pihole.password is the password of the web interface and the API, set as a ref with the backend of your choice. Without it the compile stops with Interpolation key 'applications.pihole.password' not found. An empty string turns the login off.

Include

A target includes the recipe and its options as classes and can override anything the recipe sets:

classes:
  - recipes.pihole
parameters:
  namespace: pihole
  kapitan:
    vars:
      target: pihole
  applications:
    pihole:
      password: <ref>

Fetching it

The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.

A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.1:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/pihole:1.0.1@sha256:5aac5108cd82e78eec38b8479878dd9b22d28b55ca8b36e11e47198e552f42d5
        output_path: inventory/classes/recipes/pihole

The first command fetches the recipe into inventory/classes/recipes/pihole, the second the generators and compiles:

kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch

The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/pihole@sha256:5aac5108cd82e78eec38b8479878dd9b22d28b55ca8b36e11e47198e552f42d5 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.

Settings

Setting Inventory
Pi-hole version components.pihole.image, tag and digest
time zone, Europe/London as in the compose example components.pihole.env.TZ
upstream DNS servers, Google’s 8.8.8.8 and 8.8.4.4 while unset components.pihole.env.FTLCONF_dns_upstreams, separated by ;
other settings of pihole.toml, such as dns.dnssec components.pihole.env.FTLCONF_<section>_<setting>, such as FTLCONF_dns_dnssec
exposure components.pihole.service.type, such as LoadBalancer
size of the claim etc-pihole, 1Gi components.pihole.volume_claims.etc-pihole.spec.resources.requests.storage
resources components.pihole.resources

FTL keeps its settings in /etc/pihole/pihole.toml on the claim, next to the blocklists and the query log. It treats a setting from the environment as read-only, so the web interface cannot change it.

The Service pihole is a ClusterIP Service with DNS on port 53 over TCP and UDP, HTTP on port 80 and HTTPS on port 443, where FTL serves a self-signed certificate it generates. The compose example publishes these ports on the host. Devices outside the cluster reach Pi-hole once the target exposes the Service, such as with type: LoadBalancer. Whether a load balancer serves TCP and UDP on one address depends on its implementation. FTLCONF_dns_listeningMode: ALL lets Pi-hole answer every client that reaches it, so expose port 53 only to the networks it serves, or it becomes an open resolver.

At its first start Pi-hole downloads its default blocklist, the hosts file of StevenBlack/hosts, from raw.githubusercontent.com, and a cron job in the container updates it every week.

The compose example adds three capabilities, which the recipe leaves out (DEC-29 in the spec), and FTL logs a warning for each at start. NET_ADMIN is needed only for Pi-hole’s DHCP server. SYS_TIME lets FTL’s NTP client set the clock, which in a pod is the node’s clock. SYS_NICE lets FTL raise its own priority.

The DHCP server is not set up. DHCP needs Pi-hole on the LAN, through host networking, a macvlan network or a DHCP relay, as Pi-hole’s documentation describes, and the compose example leaves its port 67/udp commented out. The NTP server that FTL runs on port 123/udp is not published either, as in the compose example.

Mapping

The service pihole becomes the component pihole. Its name stays, so no reference is rewritten.

Compose Recipe
service pihole StatefulSet pihole, Service pihole, Secret pihole with FTLCONF_webserver_api_password
pihole/pihole:latest docker.io/pihole/pihole:2026.10.1@sha256:30f803ad5dd60ff96cc0a63008a2e9271af481c1b0399a182192b2e91152c1f9, which latest was at migration
ports 53:53/tcp, 53:53/udp, 80:80/tcp and 443:443/tcp Service ports dns-tcp and dns-udp on 53, http on 80 and https on 443, ClusterIP
TZ and FTLCONF_dns_listeningMode env with the compose example’s values
FTLCONF_webserver_api_password from applications.pihole.password in the Secret pihole
bind mount ./etc-pihole at /etc/pihole claim template etc-pihole, ReadWriteOnce, 1Gi
the image’s HEALTHCHECK, dig for pi.hole at 127.0.0.1 readiness probe every 30 seconds with a timeout of 30 seconds and 3 failures, the compose defaults
cap_add with NET_ADMIN, SYS_TIME and SYS_NICE not rendered
restart: unless-stopped, container_name not rendered

Proof

check pihole --level deploys with tests/target.yml, on kind for each Kubernetes minor CI tests, with check 0.14.0. A compose file has no Kubernetes objects to compare with, so the recipe is proven by deploying it (RCP-38). tests/target.yml sets the compose example’s password and points Pi-hole’s upstream at the cluster’s DNS, kube-dns.kube-system.svc.cluster.local, because a network may not let a pod reach Google’s resolvers.

tests/smoke starts a pod of Pi-hole’s image, which carries dig and curl. From there, through the Service pihole, it

The domain lies under the reserved TLD .example, so it is NXDOMAIN until it is denied, whatever the downloaded blocklist holds.

Not tested: the downloaded blocklist, the web interface beyond its login page, and a LoadBalancer Service.

Versions

The recipe's files are in kadet-hub/recipes. All recipes