Skip to main content

Deploying Highly Available Pi-hole v6 on K3s

Introduction

This guide walks through the installation of Pi-hole v6 on a Kubernetes (K3s) cluster. Unlike a traditional standalone install, this setup leverages Cloud-Native principles to ensure:

  • High Availability: Using Kube-Vip and MetalLB to provide a persistent "Virtual IP" (VIP) for DNS queries.

  • Data Persistence: Utilizing Longhorn distributed storage so your blocklists and logs survive node failures.

  • Secure Access: Using Traefik and Cert-Manager (with Let's Encrypt) to provide HTTPS access to the web dashboard.

Prerequisites

Before applying the Helm chart, ensure your environment meets the following requirements:

Cluster Components

Networking

  • A reserved Static IP for your DNS (e.g., 192.168.100.102).

  • A registered domain or local DNS record pointing to your Ingress (e.g., pihole.homelab.penguincave.link).

Configure and Installation

Web UI Password

To have a password authentication upon installation, it's best to have a secret created so we don't have to expose it in our helm values file.

kubectl create secret generic pihole-password \
  --from-literal=password='your-secure-password' \
  -n pihole

Helm Values

The easiest way to deploy apps in Kubernetes is by Helm charts. Create a file named pihole-values.yaml. This configuration is specifically tuned for Pi-hole v6 and Kube-Vip.

# Your web UI admin password
admin:
  existingSecret: pihole-password
  passowrdKey: password

# Pi-hole v6 FTL Configuration
extraEnvVars:
  # Force Pi-hole to respond to the K8s gateway IP
  FTLCONF_dns_listeningMode: "all"
  # These help the engine recognize the LoadBalancer IP
  FTLCONF_dns_reply_host_IPv4: "192.168.100.102"
  FTLCONF_dns_reply_host_force4: "true"
# Optional, if you need API access
  FTLCONF_webserver_api: "true"

# Persistence using Longhorn
persistentVolumeClaim:
  enabled: true
  storageClass: "longhorn"
  size: 5Gi

# Service configuration
serviceDns:
  enabled: true
  type: LoadBalancer
  loadBalancerIP: "192.168.100.102"
  # CHANGE THIS: Local -> Cluster
  # This ensures any node can receive the DNS query
  externalTrafficPolicy: Local
  annotations: 
    kube-vip.io/loadbalancerIPs: "192.168.100.102"
    kube-vip.io/allow-shared-ip: "pihole-dns"

# Security Context for performance
containerSecurityContext:
  capabilities:
    add:
      - SYS_NICE
      - NET_ADMIN

# Ingress configuration for Traefik + Cert-Manager
ingress:
  enabled: true
  ingressClassName: traefik
  networkPolicy:
    enabled: false
  annotations:
    # Adding this back fixes the CLASS <none> visual bug in kubectl
    kubernetes.io/ingress.class: traefik
    cert-manager.io/cluster-issuer: "letsencrypt-cloudflare"
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
  hosts:
    - pihole.homelab.penguincave.link
  tls:
    - secretName: pihole-cert-secret
      hosts:
        - pihole.homelab.penguincave.link

# DNS Upstreams
DNS1: "1.1.1.1"
DNS2: "8.8.8.8"

# Resource optimization for Longhorn/K3s
resources:
  limits:
    cpu: 200m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

Installation

Add the Pi-hole Helm repository and install the chart:

# Add repo
helm repo add mojo2600 https://mojo2600.github.io/pihole-kubernetes/
helm repo update

# Install
helm install pihole mojo2600/pihole -n pihole -f pihole-values.yaml

Verification

Once the pods are running (kubectl get pods -n pihole), verify the setup from a remote machine:

  1. Test DNS Resolution: nslookup google.com <serviceDns.loadBalancerIP> , in this case 192.168.100.102

  2. Verify Web UI: Navigate to your ingress hostname, in this case https://pihole.homelab.penguincave.link/admin and log in with the password you set in your values file.