Managing DNS records for a homelab with dozens of services is tedious. Every time you deploy something new, you have to remember to add a DNS record. I solved this by using external-dns to automatically create Pi-hole DNS entries from Kubernetes ingress resources.
The Goal
When I create an ingress like this:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frigate
annotations:
external-dns.alpha.kubernetes.io/hostname: frigate.bowerha.us
spec:
rules:
- host: frigate.bowerha.us
# ...
I want Pi-hole to automatically create a DNS record pointing frigate.bowerha.us to my ingress controller’s IP. No manual steps, no forgetting to update DNS.
Components
- Pi-hole - DNS server and ad blocker
- external-dns - Kubernetes controller that syncs DNS records
- Kubernetes ingress-nginx - Ingress controller with a known IP
Setting Up external-dns
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: external-dns
spec:
replicas: 1
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.14.0
args:
- --source=ingress
- --provider=pihole
- --pihole-server=http://pihole-web.pihole.svc.cluster.local
- --pihole-password=$(PIHOLE_PASSWORD)
- --domain-filter=bowerha.us
- --registry=noop
- --policy=upsert-only
- --interval=1m
env:
- name: PIHOLE_PASSWORD
valueFrom:
secretKeyRef:
name: pihole-password
key: password
Key arguments:
--source=ingress- Watch ingress resources for DNS records--provider=pihole- Use Pi-hole as the DNS backend--domain-filter=bowerha.us- Only manage records for this domain--policy=upsert-only- Create/update but never delete records--interval=1m- Check for changes every minute
Pi-hole Password Secret
apiVersion: v1
kind: Secret
metadata:
name: pihole-password
namespace: external-dns
type: Opaque
stringData:
password: "your-pihole-admin-password"
RBAC
external-dns needs permission to read ingresses:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services", "endpoints", "pods"]
verbs: ["get", "watch", "list"]
- apiGroups: ["extensions", "networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "watch", "list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: external-dns
Annotating Ingresses
The key annotation is external-dns.alpha.kubernetes.io/hostname:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: home-assistant
namespace: homeassistant
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
external-dns.alpha.kubernetes.io/hostname: ha.bowerha.us
spec:
ingressClassName: nginx
rules:
- host: ha.bowerha.us
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: home-assistant
port:
number: 8123
tls:
- hosts:
- ha.bowerha.us
secretName: home-assistant-tls
Within a minute of applying this ingress, Pi-hole will have a local DNS record for ha.bowerha.us pointing to the ingress controller’s LoadBalancer IP.
How It Works
- external-dns watches for ingress resources with the hostname annotation
- It queries the ingress controller’s service to find its external IP
- It calls the Pi-hole API to create/update a Local DNS Record
- Pi-hole serves that record to all clients on the network
The records appear in Pi-hole under Local DNS > DNS Records.
Troubleshooting
external-dns CrashLoopBackOff
Usually means it can’t reach Pi-hole. Check:
- Is the Pi-hole service URL correct?
- Is Pi-hole actually running and responding on port 80?
- Is the password correct?
I had an issue where Pi-hole’s FTL process got stuck after a volume issue. Restarting Pi-hole fixed external-dns.
Records Not Appearing
Check external-dns logs:
kubectl logs -n external-dns deployment/external-dns
Look for:
- Connection errors to Pi-hole
- Domain filter mismatches
- Ingress resources missing the annotation
Wrong IP Address
external-dns gets the IP from the ingress controller’s service. If you’re using MetalLB or a cloud LoadBalancer, make sure the service has an external IP assigned:
kubectl get svc -n ingress-nginx
Making It Standard Practice
I added this to my team’s deployment guidelines in CLAUDE.md:
## DNS Management
When creating ingress resources for `bowerha.us` domains, always include
the external-dns annotation:
annotations:
external-dns.alpha.kubernetes.io/hostname: myapp.bowerha.us
This automatically creates a Pi-hole DNS record pointing to the ingress
controller. No manual DNS configuration needed.
Now DNS is just part of the deployment - no separate step to remember.
Result
What used to be a manual process:
- Deploy app
- Create ingress
- Remember to add DNS
- Log into Pi-hole
- Add Local DNS record
- Test that it works
Is now:
- Deploy app with annotated ingress
- Done
The DNS record appears automatically within a minute. When I tear down services, I use --policy=upsert-only so records persist (useful for debugging), but you could use --policy=sync for automatic cleanup.
This small automation removes friction from deploying new services and eliminates a whole category of “why can’t I reach this?” debugging sessions.