Skip to content

Pi-hole

Deploy Pi-hole DNS sinkhole on Kubernetes — network-wide ad blocking and DNS filtering for all devices on your network, with optional recursive DNS via Unbound, blocklist presets, and automated gravity database management.

serviceDns.loadBalancerIP must be fixed — changing the IP breaks DNS for all configured devices

Pi-hole acts as the DNS server for your network. Every device that points to Pi-hole stores the IP address in its DNS configuration. If serviceDns.loadBalancerIP changes after deployment, all devices will lose DNS resolution until they are manually reconfigured. Reserve and fix the IP before the first deployment and never change it.

pihole.listeningMode must be ALL for Kubernetes — the default LOCAL does not work

Pi-hole’s default DNS listening mode (LOCAL) only accepts queries from the local interface. In Kubernetes, DNS queries from pods arrive via virtual network interfaces and are not considered “local.” Set pihole.listeningMode: ALL (the chart default) to accept queries from all interfaces, including Kubernetes pod network ranges.

Key Features

  • Blocklist presetsnone, basic (170k), balanced (970k), aggressive (2.6M), gaming-friendly
  • Gravity init container — blocklists, whitelist, blacklist, and regex reconciled before startup
  • gravity.updateOnInit — runs pihole -g in a second init container (fully ready on first deploy)
  • Whitelist presets — one-flag whitelist for Microsoft, Apple, Google, gaming, smart home services
  • Conditional forwarding — resolve local domain names via your router or home DNS server
  • Unbound sidecar — recursive DNS from root servers (no third-party DNS provider required)
  • DHCP support — requires hostNetwork: true
  • Prometheus metricspihole-exporter sidecar with ServiceMonitor
  • S3 backup — gravity.db, custom DNS records, and dnsmasq config

Security Scan

Framework Score
MITRE + NSA + SOC2 86%

Security posture is acceptable with documented Pi-hole runtime exceptions for required DNS/network capabilities, root bootstrap behavior, writable Pi-hole configuration directories, and site-specific NetworkPolicy delegation.

Architecture

The chart runs a single Pi-hole Deployment with persistent storage at /etc/pihole. When gravity.enabled: true (default), two init containers run before Pi-hole starts:

  1. gravity-init — Alpine container that reconciles the gravity.db schema for Pi-hole v6. Inserts all configured adlists, whitelist, blacklist, and regex rules into the Default group.

  2. gravity-update (when gravity.updateOnInit: true) — Official Pi-hole container that runs pihole -g after gravity-init finishes. Ensures blocklists are fully downloaded before the main container starts.

Optional sidecars:

  • Unbound — recursive DNS (auto-sets upstream to 127.0.0.1#5335)
  • pihole-exporter — Prometheus metrics on port 9617

Installation

HTTPS repository:

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install pihole helmforge/pihole -f values.yaml

OCI registry:

helm install pihole oci://ghcr.io/helmforgedev/helm/pihole -f values.yaml

Deployment Examples

# values.yaml — Home network with balanced blocking and monitoring
# Fix the LoadBalancer IP before deploying — changing it breaks all devices

admin:
  existingSecret: pihole-admin-secret
  existingSecretKey: password

pihole:
  timezone: 'America/Sao_Paulo'
  upstreamDns: '1.1.1.1;1.0.0.1' # Cloudflare
  listeningMode: ALL # required for Kubernetes

dns:
  preset: balanced # StevenBlack + Hagezi Multi NORMAL (~970k domains)
  whitelistPresets:
    microsoft: true
    apple: true
    gaming: true
  customRecords:
    - '192.168.1.1 router.home'
    - '192.168.1.10 nas.home'
  conditionalForwarding:
    enabled: true
    domain: home.local
    network: 192.168.1.0/24
    router: 192.168.1.1

gravity:
  updateOnInit: true

serviceDns:
  type: LoadBalancer
  loadBalancerIP: '192.168.1.53' # must be fixed; changing breaks all devices

metrics:
  enabled: true
  serviceMonitor:
    enabled: true

persistence:
  enabled: true
  size: 2Gi

ingress:
  enabled: true
  ingressClassName: traefik
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: pihole.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: pihole-tls
      hosts:
        - pihole.example.com
# values.yaml — Privacy-first setup with Unbound recursive DNS
# Unbound queries root nameservers directly — no third-party DNS provider
# When unbound.enabled=true, upstreamDns is automatically set to 127.0.0.1#5335

admin:
  existingSecret: pihole-admin-secret

pihole:
  timezone: 'UTC'
  listeningMode: ALL
  dnssec: true # DNSSEC validation (Unbound validates the full chain)
  ftl:
    privacyLevel: 2 # hide domains and clients from query log
    cnameInspection: true
    queryLogging: true

dns:
  preset: balanced

unbound:
  enabled: true # upstream is auto-set to 127.0.0.1#5335
  extraConfig: |
    cache-min-ttl: 300
    cache-max-ttl: 86400

gravity:
  updateOnInit: true

serviceDns:
  type: LoadBalancer
  loadBalancerIP: '192.168.1.53'

backup:
  enabled: true
  schedule: '0 3 * * *'
  s3:
    endpoint: https://s3.amazonaws.com
    bucket: pihole-backups
    existingSecret: pihole-s3-credentials
  include:
    gravity: true
    customDns: true
    dnsmasq: true
# values.yaml — Pi-hole as DHCP server (requires hostNetwork)
# DHCP broadcast packets do not cross network boundaries;
# hostNetwork gives Pi-hole direct access to the node's network interface

admin:
  existingSecret: pihole-admin-secret

pihole:
  timezone: 'UTC'
  listeningMode: ALL
  ftl:
    rateLimit: 0 # disable rate limiting for DHCP environments

dns:
  preset: balanced

dhcp:
  enabled: true

hostNetwork: true # required for DHCP broadcast reception
dnsPolicy: ClusterFirstWithHostNet # auto-set when hostNetwork=true

serviceDns:
  type: ClusterIP # DNS exposed via host port when hostNetwork=true

gravity:
  updateOnInit: true
# values.yaml — Restricted network (kids/office) with aggressive blocking
admin:
  existingSecret: pihole-admin-secret

pihole:
  timezone: 'UTC'
  listeningMode: ALL
  ftl:
    queryLogging: true
    privacyLevel: 1 # hide domains from query log but keep client info

dns:
  preset: aggressive # StevenBlack + Hagezi PRO + Threat Intel (~2.6M domains)
  blacklist:
    - tiktok.com
    - snapchat.com
    - instagram.com
  regex:
    - '^ad[sxv]?[0-9]*\..*'
  whitelistPresets:
    microsoft: true # allow Windows Update and Office 365
    apple: false
    gaming: false

gravity:
  updateOnInit: true

serviceDns:
  type: LoadBalancer
  loadBalancerIP: '10.0.0.53'

Configuration Reference

Pi-hole Application

Parameter Type Default Description
pihole.timezone string UTC Timezone for logs and scheduled tasks.
pihole.upstreamDns string 8.8.8.8;8.8.4.4 Upstream DNS servers (semicolon-delimited). Auto-overridden to 127.0.0.1#5335 when Unbound is enabled.
pihole.listeningMode string ALL DNS listening mode. Must be ALL for Kubernetes. (LOCAL only works on bare metal.)
pihole.dnssec boolean false Enable DNSSEC validation.
pihole.ftl.cacheSize integer 10000 DNS cache size.
pihole.ftl.privacyLevel integer 0 0=show all, 1=hide domains, 2=hide domains+clients, 3=anonymous.
pihole.ftl.rateLimit integer 1000 Rate-limit per client (queries per interval). 0 to disable.
pihole.ftl.queryLogging boolean true Enable query logging.
admin.password string "" Admin password. Auto-generated if empty.
admin.existingSecret string "" Existing secret with admin password.
admin.existingSecretKey string password Key for the password in the existing secret.

Blocklists and DNS

Parameter Type Default Description
dns.preset string none Blocklist preset: none, basic (170k), balanced (970k), aggressive (2.6M), gaming-friendly.
dns.adlists array [] Custom blocklist URLs (appended to the preset).
dns.whitelistPresets.microsoft boolean false Whitelist Microsoft services (Windows Update, Office 365).
dns.whitelistPresets.apple boolean false Whitelist Apple services (iCloud, App Store).
dns.whitelistPresets.gaming boolean false Whitelist gaming platforms (Xbox, PSN, Nintendo, Steam).
dns.whitelist array [] Additional whitelisted domains.
dns.blacklist array [] Blacklisted domains (exact match).
dns.regex array [] Regex filters for blocking (advanced).
dns.customRecords array [] Local DNS A records: "IP HOSTNAME".
dns.cnameRecords array [] Custom CNAME records (dnsmasq format).
dns.customDnsmasq array [] Raw dnsmasq configuration lines.
dns.conditionalForwarding.enabled boolean false Forward local domain to router for reverse lookups.
dns.conditionalForwarding.domain string "" Local domain (e.g., home.local).
dns.conditionalForwarding.router string "" Router IP for reverse lookups.

Services and Networking

Parameter Type Default Description
serviceDns.type string LoadBalancer DNS service type. Use LoadBalancer for external network exposure.
serviceDns.loadBalancerIP string Fixed IP for the DNS service. Reserve before deploying and never change.
serviceWeb.type string ClusterIP Web admin service type.
ingress.enabled boolean false Enable Ingress for the web admin interface.
hostNetwork boolean false Use host network stack. Required for DHCP.
dnsPolicy string "" Pod DNS policy. Auto-set to ClusterFirstWithHostNet when hostNetwork: true.

Security and Resources

Parameter Type Default Description
resources object requests 100m/128Mi, limits 500m/512Mi CPU and memory resources for the Pi-hole container.
gravity.resources object requests 50m/64Mi, limits 200m/128Mi CPU and memory resources for the gravity schema/list init container.
gravity.updateResources object requests 100m/128Mi, limits 500m/512Mi CPU and memory resources for the gravity update init container.
metrics.resources object requests 25m/64Mi, limits 100m/128Mi CPU and memory resources for the optional pihole-exporter sidecar.
unbound.resources object requests 50m/64Mi, limits 200m/128Mi CPU and memory resources for the optional Unbound sidecar.
backup.resources object requests 50m/64Mi, limits 250m/256Mi CPU and memory resources for backup and upload containers.
podSecurityContext.seccompProfile.type string RuntimeDefault Enables the Kubernetes default seccomp profile.
securityContext.allowPrivilegeEscalation boolean false Prevents privilege escalation while keeping Pi-hole’s required capabilities.
serviceAccount.automountServiceAccountToken boolean false Keeps Kubernetes API tokens out of the pod unless explicitly needed.

Gravity and Unbound

Parameter Type Default Description
gravity.enabled boolean true Run gravity-init init container to reconcile lists before startup.
gravity.updateOnInit boolean true Run pihole -g in a second init container (full download before ready).
unbound.enabled boolean false Deploy Unbound recursive DNS sidecar. Auto-overrides upstream DNS.
unbound.port integer 5335 Internal Unbound listen port.
unbound.config string "" Full unbound.conf override. Replaces the chart-rendered file.
unbound.extraConfig string "" Extra directives appended inside the default server: section.

The chart mounts a generated unbound.conf over the image default so Unbound binds to 127.0.0.1 on unbound.port and does not conflict with Pi-hole DNS on port 53 in the same pod network namespace. The default config uses the DNSSEC trust anchor generated by the mvance/unbound image at /opt/unbound/etc/unbound/var/root.key and intentionally omits root-hints, because the image does not ship a root.hints file.

Use unbound.extraConfig for small additions to the default server block. Use unbound.config only when you need to replace the whole file; when it is set, unbound.extraConfig is ignored.

Metrics and Backup

Parameter Type Default Description
metrics.enabled boolean false Deploy pihole-exporter Prometheus sidecar.
metrics.port integer 9617 Metrics endpoint port.
metrics.serviceMonitor.enabled boolean false Create Prometheus ServiceMonitor resource.
backup.enabled boolean false Enable scheduled S3 backup.
backup.schedule string "0 3 * * *" Cron schedule.
backup.include.gravity boolean true Include gravity.db in backup.
backup.include.customDns boolean true Include custom DNS records in backup.
backup.include.dnsmasq boolean true Include dnsmasq configuration in backup.
backup.s3.existingSecret string "" Existing secret with S3 credentials.
persistence.enabled boolean true Enable PVC for /etc/pihole.
persistence.size string 1Gi PVC size.
extraManifests array [] Extra Kubernetes manifests.

More Information