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

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

ParameterTypeDefaultDescription
pihole.timezonestringUTCTimezone for logs and scheduled tasks.
pihole.upstreamDnsstring8.8.8.8;8.8.4.4Upstream DNS servers (semicolon-delimited). Auto-overridden to 127.0.0.1#5335 when Unbound is enabled.
pihole.listeningModestringALLDNS listening mode. Must be ALL for Kubernetes. (LOCAL only works on bare metal.)
pihole.dnssecbooleanfalseEnable DNSSEC validation.
pihole.ftl.cacheSizeinteger10000DNS cache size.
pihole.ftl.privacyLevelinteger00=show all, 1=hide domains, 2=hide domains+clients, 3=anonymous.
pihole.ftl.rateLimitinteger1000Rate-limit per client (queries per interval). 0 to disable.
pihole.ftl.queryLoggingbooleantrueEnable query logging.
admin.passwordstring""Admin password. Auto-generated if empty.
admin.existingSecretstring""Existing secret with admin password.
admin.existingSecretKeystringpasswordKey for the password in the existing secret.

Blocklists and DNS

ParameterTypeDefaultDescription
dns.presetstringnoneBlocklist preset: none, basic (170k), balanced (970k), aggressive (2.6M), gaming-friendly.
dns.adlistsarray[]Custom blocklist URLs (appended to the preset).
dns.whitelistPresets.microsoftbooleanfalseWhitelist Microsoft services (Windows Update, Office 365).
dns.whitelistPresets.applebooleanfalseWhitelist Apple services (iCloud, App Store).
dns.whitelistPresets.gamingbooleanfalseWhitelist gaming platforms (Xbox, PSN, Nintendo, Steam).
dns.whitelistarray[]Additional whitelisted domains.
dns.blacklistarray[]Blacklisted domains (exact match).
dns.regexarray[]Regex filters for blocking (advanced).
dns.customRecordsarray[]Local DNS A records: "IP HOSTNAME".
dns.cnameRecordsarray[]Custom CNAME records (dnsmasq format).
dns.customDnsmasqarray[]Raw dnsmasq configuration lines.
dns.conditionalForwarding.enabledbooleanfalseForward local domain to router for reverse lookups.
dns.conditionalForwarding.domainstring""Local domain (e.g., home.local).
dns.conditionalForwarding.routerstring""Router IP for reverse lookups.

Services and Networking

ParameterTypeDefaultDescription
serviceDns.typestringLoadBalancerDNS service type. Use LoadBalancer for external network exposure.
serviceDns.loadBalancerIPstringFixed IP for the DNS service. Reserve before deploying and never change.
serviceWeb.typestringClusterIPWeb admin service type.
ingress.enabledbooleanfalseEnable Ingress for the web admin interface.
hostNetworkbooleanfalseUse host network stack. Required for DHCP.
dnsPolicystring""Pod DNS policy. Auto-set to ClusterFirstWithHostNet when hostNetwork: true.

Gravity and Unbound

ParameterTypeDefaultDescription
gravity.enabledbooleantrueRun gravity-init init container to reconcile lists before startup.
gravity.updateOnInitbooleantrueRun pihole -g in a second init container (full download before ready).
unbound.enabledbooleanfalseDeploy Unbound recursive DNS sidecar. Auto-overrides upstream DNS.
unbound.portinteger5335Internal Unbound listen port.
unbound.configstring""Full unbound.conf override. Replaces the chart-rendered file.
unbound.extraConfigstring""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

ParameterTypeDefaultDescription
metrics.enabledbooleanfalseDeploy pihole-exporter Prometheus sidecar.
metrics.portinteger9617Metrics endpoint port.
metrics.serviceMonitor.enabledbooleanfalseCreate Prometheus ServiceMonitor resource.
backup.enabledbooleanfalseEnable scheduled S3 backup.
backup.schedulestring"0 3 * * *"Cron schedule.
backup.include.gravitybooleantrueInclude gravity.db in backup.
backup.include.customDnsbooleantrueInclude custom DNS records in backup.
backup.include.dnsmasqbooleantrueInclude dnsmasq configuration in backup.
backup.s3.existingSecretstring""Existing secret with S3 credentials.
persistence.enabledbooleantrueEnable PVC for /etc/pihole.
persistence.sizestring1GiPVC size.
extraManifestsarray[]Extra Kubernetes manifests.

More Information