Kubernetes部署Pi-hole:云原生家庭网络广告拦截方案实践
1. 项目概述当Pi-hole遇上Kubernetes如果你和我一样既是家庭网络的管理员又是一名Kubernetes的爱好者那你肯定想过一个问题能不能把那个好用的网络广告拦截器Pi-hole也塞进我的K8s集群里让它像其他微服务一样通过声明式配置来管理答案是肯定的而且这件事已经有人帮我们铺好了路。MoJo2600维护的这个pihole-kubernetes项目就是一个专门为Kubernetes环境打造的Helm Chart仓库它让在K8s上部署和管理Pi-hole变得像helm install一样简单。Pi-hole本身是一个强大的网络级广告和追踪器拦截器通过充当本地DNS服务器将广告域名解析到“黑洞”从而保护整个局域网内的设备。而Kubernetes提供了无与伦比的编排、自愈和扩展能力。将两者结合意味着你可以获得一个高可用、易于备份和迁移、并且能通过GitOps进行版本控制的家庭网络核心服务。这个Helm Chart抽象了Pi-hole在K8s中运行所需的所有复杂性包括持久化存储、服务暴露、配置管理以及关键的DNS上游服务器设置。无论你是在家里的树莓派集群还是在云上的小型VPS集群中运行Kubernetes这个方案都能让你以云原生的方式获得一个干净、快速、自主可控的网络环境。2. 核心架构与设计思路拆解2.1 为什么选择Helm Chart部署Pi-hole在Kubernetes中部署一个像Pi-hole这样有状态、且需要特殊网络权限的应用直接使用裸的YAML文件会非常繁琐。你需要处理Deployment、Service、PersistentVolumeClaim、ConfigMap、Secret等多个资源对象并且要确保它们之间的依赖关系和配置同步。Helm作为Kubernetes的包管理器完美地解决了这个问题。pihole-kubernetes这个Chart将所有这些资源打包成一个可参数化的发行版。使用Helm Chart的核心优势在于可重复性和可配置性。通过一个values.yaml文件你可以定义所有部署参数比如Pi-hole的Web管理界面密码、使用的时区、要拦截的广告列表来源、DNS上游服务器等。这意味着你的整个Pi-hole实例配置可以被版本化与你的集群基础设施代码存放在一起。当需要在新环境部署或重建时一条命令就能还原出完全一致的状态。这对于家庭实验室的灾难恢复来说价值巨大。2.2 Chart的核心组件与交互关系这个Chart部署的不仅仅是一个Pi-hole的Docker容器。它是一个为Kubernetes环境量身定制的完整应用栈。我们来拆解一下它的核心组件Pi-hole主容器基于pihole/pihole官方镜像这是广告拦截的核心逻辑所在。Chart会通过环境变量和Volume挂载将配置如管理员密码、DNS设置和数据如查询日志、自定义列表持久化。持久化存储广告拦截的统计信息、自定义白名单/黑名单、长期查询日志都需要持久化。Chart默认会创建PersistentVolumeClaim (PVC)将/etc/pihole和/etc/dnsmasq.d等目录挂载到持久卷上确保Pod重启或重新调度后数据不丢失。Service对象这是关键。Pi-hole需要暴露两个主要端口DNS服务端口 (UDP/TCP 53)这是它作为DNS服务器工作的端口局域网内的设备需要将DNS指向这个服务。Web管理端口 (TCP 80)用于访问管理后台查看统计数据、管理过滤列表。 Chart创建的Service对象为这些端口提供了稳定的集群内部访问端点。网络策略与权限Pi-hole需要以NET_ADMIN等Linux能力运行以便能够绑定到53端口这是一个特权端口。Chart在Pod的安全上下文Security Context中进行了相应配置。同时考虑到DNS服务的高可用性通常会以hostNetwork: true模式运行或者通过LoadBalancer Service对外暴露这涉及到与metallb这类负载均衡器控制器的集成。配置管理敏感信息如WEBPASSWORD通过Kubernetes Secret管理而非明文写在配置里。其他通用配置则通过ConfigMap或Chart的values.yaml直接注入环境变量。整个设计思路是**“开箱即用但深度可定制”**。默认配置足以让Pi-hole在集群内跑起来但通过覆盖values.yaml中的数百个参数你可以精细调整每一个环节使其完美适配你的特定网络环境和Kubernetes集群配置。3. 前期准备与环境配置要点在运行helm install之前我们需要确保Kubernetes集群已经做好了准备。这一步常常被忽略但却是后续一切顺利的基石。3.1 Kubernetes集群基础要求首先你需要一个正在运行的Kubernetes集群。对于家庭实验室我强烈推荐使用k3s或k0s它们轻量且易于安装。一个单节点的集群就足以运行Pi-hole但如果你有多节点可以借此实现高可用。确保你的kubectl能够正常连接到集群并且Helm 3已经安装完毕。注意Pi-hole的DNS服务需要占用主机的53端口。如果你在Kubernetes节点上比如你的家庭服务器已经运行了systemd-resolved或其他DNS服务它们会占用53端口导致Pi-hole的Pod无法启动。你需要先禁用或重新配置这些服务。例如在Ubuntu上可以编辑/etc/systemd/resolved.conf设置DNSStubListenerno然后重启systemd-resolved服务。3.2 关键依赖MetalLB负载均衡器这是将Pi-hole服务暴露到家庭局域网的关键一步。Kubernetes默认的Service类型有ClusterIP仅集群内访问、NodePort通过节点端口访问和LoadBalancer需要云提供商。在家庭环境我们需要MetalLB来为LoadBalancer类型的Service提供实际的IP地址。安装MetalLBkubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml安装后需要创建一个IP地址池配置。假设你的家庭路由器网段是192.168.1.0/24并且路由器DHCP池分配的是192.168.1.100-200那么你可以划出一段静态IP比如192.168.1.210-192.168.1.220给MetalLB使用。创建一个名为metallb-config.yaml的文件apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: default-pool namespace: metallb-system spec: addresses: - 192.168.1.210-192.168.1.220 --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: default namespace: metallb-system spec: ipAddressPools: - default-pool然后应用它kubectl apply -f metallb-config.yaml。这样当Pi-hole的Service类型设置为LoadBalancer时MetalLB就会从210-220这个池子里分配一个IP给它这个IP在你的局域网内是直接可达的。3.3 持久化存储准备Pi-hole需要持久化存储来保存配置和日志。你需要确保你的Kubernetes集群有可用的存储类StorageClass。在大多数家庭实验室环境中如使用k3s会默认提供一个基于本地路径local-path的StorageClass。你可以通过kubectl get storageclass来查看。如果使用云服务或有独立的NAS也可以配置NFS、Ceph等网络存储类这对于多节点集群的高可用部署更有优势。实操心得对于单节点家庭集群使用local-path存储类是最简单直接的性能也最好。但要注意这会将数据绑定到特定节点。如果该节点宕机Pod虽然可以被调度到其他节点但由于数据不在那新的Pod将无法读取旧数据。因此定期备份/etc/pihole目录下的数据特别是custom.list和gravity.db是一个好习惯。对于真正的生产级高可用应考虑使用网络存储。4. 部署与配置全流程解析环境就绪后我们就可以开始部署Pi-hole了。整个过程分为添加仓库、定制配置、安装部署和验证服务几个步骤。4.1 添加Helm仓库与基础安装首先按照项目说明添加Helm仓库并更新本地索引helm repo add mojo2600 https://mojo2600.github.io/pihole-kubernetes/ helm repo update添加成功后你可以查看这个仓库里有哪些Charthelm search repo mojo2600。通常主要的就是pihole这个Chart。最基础的安装命令是helm install my-pihole mojo2600/pihole这条命令会使用Chart的所有默认值在默认命名空间default下部署一个名为my-pihole的Pi-hole实例。但这样部署出来的Pi-holeWeb密码是随机的DNS上游服务器是Cloudflare并且服务类型是ClusterIP只能在集群内部访问。这显然不符合我们作为家庭DNS服务器的需求。因此定制化配置是必须的。4.2 深度定制values.yaml配置文件我们需要创建一个自定义的values.yaml文件来覆盖默认配置。这是整个部署的核心环节。下面我以一个典型的家庭网络配置为例解释关键参数# pihole-custom-values.yaml image: tag: latest # 或指定一个稳定版本如2023.05.1 # 1. 设置管理员密码 (必改) # 这将创建一个Secret来存储密码。请务必修改 webPassword: YourStrongPasswordHere # 2. 持久化存储配置 persistence: enabled: true size: 10Gi # 10GB对于家庭使用绰绰有余 # storageClassName: local-path # 如果不指定则使用集群默认StorageClass # 3. 服务暴露配置 (关键) service: dns: type: LoadBalancer # 使用LoadBalancer让MetalLB分配IP # 如果你希望使用hostNetwork模式性能更好但一个节点只能运行一个实例可以这样 # type: ClusterIP # hostNetwork: true # 同时在Pod spec中启用hostNetwork loadBalancerIP: 192.168.1.210 # 可以指定一个固定的MetalLB IP annotations: metallb.universe.tf/allow-shared-ip: true # 可选允许多个服务共享IP不同端口 http: type: LoadBalancer loadBalancerIP: 192.168.1.210 # 可以让DNS和Web管理共用同一个IP使用不同端口 # 或者为Web管理单独分配一个IP如 192.168.1.211 # 4. DNS配置 (核心功能) # 设置Pi-hole上游DNS服务器。这里使用Cloudflare和Google的DNS-over-TLS提升隐私和安全性。 DNS1: 1.1.1.1 DNS2: 8.8.8.8 # 如果你想使用DNS-over-TLS (DoT): # DNSSEC: true # DNS_BOGUS_PRIV: false # 更高级的配置可以通过extraEnvVars传递。 # 5. 自定义广告列表 # Pi-hole默认使用一些列表你可以在这里添加更多。 # 这些列表会通过pihole -g命令更新到gravity数据库中。 adlists: - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts - https://mirror1.malwaredomains.com/files/justdomains - https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt # 6. 时区与本地化 TZ: Asia/Shanghai # 设置为你的本地时区让日志时间正确 WEBTHEME: default-dark # 可选暗色主题更护眼 # 7. 资源限制 (根据你的节点配置调整) resources: requests: memory: 128Mi cpu: 100m limits: memory: 512Mi cpu: 500m参数解析与避坑指南webPassword这是你访问Web管理界面http://loadBalancerIP/admin的密码。Chart会用它生成一个Secret。务必在首次安装前设置一个强密码否则你需要进入Pod内部手动运行pihole -a -p来修改比较麻烦。service.dns.type设置为LoadBalancer是最通用的方式。如果你追求极致的DNS解析性能减少一次网络跳转可以考虑使用hostNetwork: true模式。但这意味着Pod会直接使用宿主机的网络命名空间需要确保53端口在宿主机上可用且每个节点只能运行一个此类Pod。loadBalancerIP指定一个IP可以让服务地址固定方便你在路由器中设置静态DNS指向。如果不指定MetalLB会每次动态分配。DNS1/DNS2这是Pi-hole向上游查询的DNS服务器。不要把它设为你路由器的IP否则就是循环查询了。推荐使用公共的、可信的DNS如Cloudflare (1.1.1.1) 或 Google (8.8.8.8)。对于注重隐私的用户可以考虑使用DNS-over-TLS或DNS-over-HTTPS的上游。adlists广告列表是Pi-hole的“武器库”。默认列表已经不错但添加更多列表可以提高拦截率。注意列表越多更新gravity数据库耗时越长内存占用也可能略增。建议从几个主流列表开始根据需求调整。4.3 执行安装与验证使用自定义的values文件进行安装helm install -f pihole-custom-values.yaml my-pihole mojo2600/pihole --namespace pihole --create-namespace这里我们指定了命名空间pihole--create-namespace会在不存在时自动创建它这有助于资源隔离和管理。安装命令执行后使用以下命令观察部署状态# 查看Pod状态等待其变为Running kubectl get pods -n pihole -w # 查看Service和分配的External-IP (即MetalLB分配的IP) kubectl get svc -n pihole # 查看Pod日志排查可能的问题 kubectl logs -f deployment/my-pihole-pihole -n pihole当Pod状态为Running并且Service的EXTERNAL-IP栏显示了一个IP地址如192.168.1.210时说明部署基本成功。4.4 配置家庭网络使用Pi-hole DNS部署成功只是第一步接下来需要让你的家庭网络设备使用这个DNS服务。获取服务IP从kubectl get svc -n pihole的输出中找到my-pihole-pihole-dns这个Service的EXTERNAL-IP。假设是192.168.1.210。配置路由器推荐登录你的家庭路由器管理后台通常是192.168.1.1在DHCP服务器设置中将主DNS服务器和备用DNS服务器都设置为192.168.1.210。这样所有通过DHCP获取IP的设备手机、电脑、智能电视等都会自动使用Pi-hole进行DNS解析。这是最彻底、最省心的方式。手动配置设备如果不想改路由器设置可以在每个设备的网络设置中手动指定DNS服务器为192.168.1.210。验证在浏览器中访问http://192.168.1.210/admin使用你设置的webPassword登录。在管理面板的仪表盘上你应该能看到查询请求。也可以打开一个终端执行nslookup doubleclick.net 192.168.1.210如果返回0.0.0.0说明广告域名已被成功拦截。5. 高级配置与运维管理基础部署完成后我们可以进一步优化和加固这个Pi-hole实例。5.1 配置DNS-over-TLS (DoT) 上游为了提高DNS查询的隐私性和安全性避免被运营商窥探或篡改我们可以让Pi-hole使用DNS-over-TLS向上游服务器查询。这需要在values.yaml中通过extraEnvVars添加高级环境变量。# 在原有的values.yaml中增加以下部分 extraEnvVars: - name: PIHOLE_DNS_ value: 127.0.0.1#5053 - name: DNS1 value: 1.1.1.1 - name: DNS2 value: 8.8.8.8 - name: DNSSEC value: true - name: REV_SERVER value: false # 注意这种配置需要Pi-hole容器内有一个DoT代理如cloudflared。 # 这个Chart的默认镜像可能不包含。你需要确保使用的镜像支持或者通过sidecar容器模式注入cloudflared。更常见的做法是使用一个独立的DoT代理如cloudflared作为Sidecar容器然后让Pi-hole的DNS指向这个Sidecar。这需要对Chart进行更复杂的定制可能需要修改templates/deployment.yaml。对于大多数家庭用户使用普通的DNSDNS1/DNS2已经足够安全DoT的配置属于进阶需求。5.2 设置定期重力更新Gravity Update广告列表不是一成不变的新的广告域名会不断出现。Pi-hole通过pihole -g命令来更新它的“重力”数据库。在Kubernetes中我们可以通过CronJob来定期执行这个任务。你可以创建一个如下的CronJob资源apiVersion: batch/v1 kind: CronJob metadata: name: pihole-gravity-update namespace: pihole spec: schedule: 0 3 * * * # 每天凌晨3点执行 jobTemplate: spec: template: spec: containers: - name: update image: pihole/pihole:latest command: [pihole, -g] env: - name: WEBPASSWORD valueFrom: secretKeyRef: name: my-pihole-pihole-web-password # 替换为实际的Secret名 key: password restartPolicy: OnFailure这个CronJob会创建一个临时的Pod执行更新命令后退出。你需要确保这个Pod能访问到Pi-hole的持久化数据卷或者通过API来触发更新。另一种更简单的方式是直接进入Pi-hole的管理后台在“设置” - “系统”中启用自动更新。5.3 备份与恢复策略你的Pi-hole配置白名单、黑名单、DNS记录和统计数据都存储在持久化卷中。定期备份至关重要。备份最简单的方法是使用kubectl cp命令将关键目录复制出来。# 获取Pod名称 POD_NAME$(kubectl get pods -n pihole -l app.kubernetes.io/namepihole -o jsonpath{.items[0].metadata.name}) # 备份配置和列表 kubectl cp pihole/$POD_NAME:/etc/pihole ./pihole-backup/ # 备份dnsmasq配置 kubectl cp pihole/$POD_NAME:/etc/dnsmasq.d ./dnsmasq-backup/你可以将这个过程写成一个脚本并添加到服务器的cron定时任务中。恢复恢复时需要先删除现有的PVC如果数据已损坏然后用备份文件覆盖新Pod挂载的卷初始内容。更优雅的方式是使用Velero这类Kubernetes备份工具对整个命名空间进行备份和恢复。5.4 监控与日志Pi-hole自带一个不错的Web仪表盘可以查看实时查询、拦截统计等。此外你还可以查看Pod日志kubectl logs -f -n pihole deployment/my-pihole-pihole可以查看Pi-hole的实时日志对于调试DNS问题很有帮助。集成Prometheus/Grafana社区有非官方的Pi-hole Exporter可以将Pi-hole的指标暴露给Prometheus然后在Grafana中制作精美的监控看板。这需要额外的部署步骤但能提供历史趋势分析和告警能力。资源监控使用kubectl top pod -n pihole查看Pod的CPU和内存使用情况确保资源限制设置合理。6. 常见问题与故障排查实录即使按照步骤操作也可能会遇到一些问题。这里记录了一些我亲自踩过的坑和解决方案。6.1 Pod启动失败CrashLoopBackOff这是最常见的问题。通常有几个原因端口冲突日志中可能出现listen tcp 0.0.0.0:53: bind: address already in use。这表示宿主机或Pod网络的53端口已被占用。排查在Kubernetes节点上运行sudo netstat -tulpn | grep :53。解决如果是systemd-resolved按3.1节的方法禁用它。如果是其他服务如dnsmasq、bind需要停止或修改其配置。持久化卷权限问题Pi-hole容器可能以非root用户运行而持久化卷挂载的目录权限不正确导致无法写入。排查查看Pod日志是否有Permission denied错误。解决在values.yaml中可以尝试设置Pod的安全上下文或者确保StorageClass创建的卷有正确的权限。一个快速测试方法是临时将persistence.enabled设为false如果Pod能正常启动就是权限问题。镜像拉取失败网络问题导致无法拉取pihole/pihole镜像。解决检查节点网络或尝试使用其他镜像仓库的镜像可能需要修改values.yaml中的image.repository。6.2 DNS服务不响应或无法访问Pod运行正常但设备无法通过它的IP进行DNS解析。MetalLB配置问题Service没有获得External-IP。排查kubectl describe svc my-pihole-pihole-dns -n pihole查看Events部分。常见问题是IP地址池已耗尽或配置错误。解决检查MetalLB的IP地址池配置确保IP范围正确且未被占用。防火墙/网络策略拦截节点防火墙或Kubernetes NetworkPolicy阻止了53端口的流量。排查在集群节点上尝试从局域网另一台电脑telnet loadBalancerIP 53。如果不通可能是防火墙问题。解决在节点上开放53端口UDP和TCP。例如对于ufwsudo ufw allow 53。DNS环路错误地将Pi-hole的上游DNSDNS1设置为了你路由器的IP而路由器又将DNS指向了Pi-hole形成死循环。解决确保values.yaml中的DNS1和DNS2是公共DNS或上游ISP的DNS而不是你本地网络的网关IP。6.3 Web管理界面无法登录密码错误你可能忘记了在values.yaml中设置的webPassword或者安装时使用了默认的随机密码。解决获取密码kubectl get secret my-pihole-pihole-web-password -n pihole -o jsonpath{.data.password} | base64 -d。如果不行可以进入Pod内部重置kubectl exec -it -n pihole deployment/my-pihole-pihole -- pihole -a -p然后按照提示设置新密码。Service类型或端口错误Web服务没有正确暴露。排查确认service.http.type是LoadBalancer或NodePort并且有对应的外部IP或节点端口。解决使用kubectl get svc -n pihole确认HTTP服务的访问端点。6.4 广告拦截失效设备使用了Pi-hole的DNS但广告依然出现。DNS缓存设备或路由器有DNS缓存。电脑可以尝试ipconfig /flushdnsWindows或sudo dscacheutil -flushcachemacOS。路由器可能需要重启。HTTPS广告Pi-hole只能拦截基于域名的DNS请求。一些应用或网站使用HTTPS且广告内容来自同一域名如YouTubePi-hole无法拦截。这是其工作原理的限制。列表未生效新添加的广告列表需要更新重力数据库。解决登录Web管理界面点击“工具” - “更新重力”或通过CronJob定期执行pihole -g。设备使用了硬编码的DNS某些设备如一些智能电视、IoT设备会忽略DHCP下发的DNS设置使用硬编码的DNS如8.8.8.8。解决这是最棘手的情况。终极方案是在网络层进行DNS劫持即在你路由器的防火墙规则中将所有出站的53端口UDP流量重定向到Pi-hole的IP。这需要你的路由器支持自定义防火墙规则如OpenWrt, pfSense等。6.5 性能与资源问题Pi-hole在运行时占用资源很少但在执行重力更新pihole -g时可能会消耗较多的CPU和内存特别是列表很大时。症状Pod在更新时变慢甚至可能因内存不足OOM被杀死。监控使用kubectl top pod观察资源使用峰值。调整在values.yaml中适当增加resources.limits.memory例如增加到1Gi。同时可以考虑将重力更新的CronJob调度到凌晨网络空闲时段。将Pi-hole部署到Kubernetes集群绝不仅仅是为了“炫技”。它带来的真正价值是运维的现代化。你的网络核心服务现在拥有了版本历史、一键部署/回滚、声明式配置、资源限制和健康检查。当你的树莓派SD卡损坏时你不再需要回忆Pi-hole的复杂配置只需要一条Helm命令。当你搬家或升级硬件时整个服务可以无缝迁移。这个项目提供的Helm Chart正是连接经典网络服务与现代云原生架构之间那座坚实的桥梁。从我自己的使用经验来看稳定性远超在单独虚拟机或容器中的部署与集群的其他服务如Home Assistant、Nextcloud的集成管理也方便了许多。如果遇到问题别忘了去项目的GitHub仓库看看Issues和Discussions那里有大量来自全球用户的实践分享。