- Deploy PowerDNS on k3s with SQLite backend - Add DNS ports 53 UDP/TCP to security group - Configure zones for superviber.com, muffinlabs.ai, letemcook.com, appbasecamp.com, thanksforborrowing.com - Add deploy-powerdns.sh standalone deployment script - Document in RFC 0003 Glue records updated at GoDaddy to point ns1/ns2 to 3.218.167.115. DNS verified working via Google DNS (8.8.8.8). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.7 KiB
9.7 KiB
RFC 0003: PowerDNS Self-Hosted DNS
| Status | Implemented |
| Created | 2026-01-24 |
| Related | coherence-mcp RFC 0046 |
Problem
We have 5 domains registered at GoDaddy that need DNS management:
- superviber.com
- muffinlabs.ai
- letemcook.com
- appbasecamp.com
- thanksforborrowing.com
Current state:
- Glue records exist at GoDaddy pointing ns1/ns2.superviber.com to IPs 3.12.26.86 and 3.140.40.205
- No DNS server running at those IPs (planned EKS infrastructure never deployed)
- Forgejo running at 3.218.167.115 on k3s (hearth minimal), but DNS not configured
- Cloudflare partially configured for superviber.com but nameservers not switched
We want self-hosted DNS for:
- Control over all records without third-party dashboard
- Integration with other self-hosted services (mail, git, auth)
- Single source of truth in infrastructure-as-code
Goals
- Deploy PowerDNS on existing k3s infrastructure (hearth)
- Configure zones for all 5 managed domains
- Expose DNS via port 53 UDP/TCP
- Update glue records at GoDaddy to point to working DNS servers
- Add git.beyondtheuniverse.superviber.com record pointing to Forgejo
Non-Goals
- HA DNS with 3 separate IPs (future scope, current setup is single-user)
- DNSSEC (future scope)
- Secondary/slave DNS (future scope)
- DNS-over-HTTPS or DNS-over-TLS (future scope)
Proposal
Architecture
Deploy PowerDNS on the existing hearth k3s instance (3.218.167.115) as a single authoritative DNS server.
┌─────────────────────────────────────┐
│ EC2: 3.218.167.115 │
│ (hearth-forgejo) │
│ │
┌─────────────┐ │ ┌─────────────────────────────┐ │
│ GoDaddy │ │ │ k3s │ │
│ Glue: │ │ │ │ │
│ ns1/ns2 ────┼────────►│ │ ┌─────────┐ ┌──────────┐ │ │
│ .superviber │ :53 │ │ │PowerDNS │ │ Forgejo │ │ │
│ .com │ │ │ │ :53 │ │ :3000 │ │ │
└─────────────┘ │ │ └─────────┘ └──────────┘ │ │
│ │ │ │
│ │ Traefik (ingress) │ │
│ │ :80, :443, :22 │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
Why Single Instance
For a ~1 user personal infrastructure:
- Simpler ops (one machine to manage)
- Lower cost (~$7.50/month total)
- Acceptable availability (spot instance with persistent EIP)
- GoDaddy remains as backup (can revert NS records if needed)
DNS Records
Initial zones will include:
superviber.com:
@ A 3.218.167.115
ns1 A 3.218.167.115
ns2 A 3.218.167.115
beyondtheuniverse A 3.218.167.115
git.beyondtheuniverse A 3.218.167.115
mail.beyondtheuniverse A 3.218.167.115
auth.beyondtheuniverse A 3.218.167.115
@ NS ns1.superviber.com.
@ NS ns2.superviber.com.
Other domains (muffinlabs.ai, etc.) will have minimal records pointing to the same infrastructure.
Implementation
Phase 1: Deploy PowerDNS on k3s
Add to hearth user-data.sh or apply directly:
apiVersion: v1
kind: Namespace
metadata:
name: dns
---
apiVersion: v1
kind: Secret
metadata:
name: powerdns-api-key
namespace: dns
type: Opaque
stringData:
api-key: "GENERATED_SECRET_KEY"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: powerdns
namespace: dns
spec:
replicas: 1
selector:
matchLabels:
app: powerdns
template:
metadata:
labels:
app: powerdns
spec:
containers:
- name: powerdns
image: powerdns/pdns-auth-49:4.9.2
ports:
- name: dns-udp
containerPort: 53
protocol: UDP
- name: dns-tcp
containerPort: 53
protocol: TCP
- name: api
containerPort: 8081
protocol: TCP
env:
- name: PDNS_AUTH_API_KEY
valueFrom:
secretKeyRef:
name: powerdns-api-key
key: api-key
args:
- --api=yes
- --api-key=$(PDNS_AUTH_API_KEY)
- --webserver=yes
- --webserver-address=0.0.0.0
- --webserver-port=8081
- --launch=gsqlite3
- --gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
volumeMounts:
- name: data
mountPath: /var/lib/powerdns
volumes:
- name: data
hostPath:
path: /data/powerdns
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: powerdns
namespace: dns
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: powerdns
ports:
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: api
port: 8081
targetPort: 8081
protocol: TCP
Phase 2: Update Security Group
Add to hearth main.tf security group:
# DNS (UDP)
ingress {
description = "DNS UDP"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
# DNS (TCP)
ingress {
description = "DNS TCP"
from_port = 53
to_port = 53
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
Phase 3: Configure Zones
Use PowerDNS API to create zones:
# Create zone
curl -X POST http://localhost:8081/api/v1/servers/localhost/zones \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "superviber.com.",
"kind": "Native",
"nameservers": ["ns1.superviber.com.", "ns2.superviber.com."]
}'
# Add records
curl -X PATCH http://localhost:8081/api/v1/servers/localhost/zones/superviber.com. \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rrsets": [
{
"name": "superviber.com.",
"type": "A",
"ttl": 300,
"changetype": "REPLACE",
"records": [{"content": "3.218.167.115", "disabled": false}]
},
{
"name": "ns1.superviber.com.",
"type": "A",
"ttl": 3600,
"changetype": "REPLACE",
"records": [{"content": "3.218.167.115", "disabled": false}]
},
{
"name": "ns2.superviber.com.",
"type": "A",
"ttl": 3600,
"changetype": "REPLACE",
"records": [{"content": "3.218.167.115", "disabled": false}]
},
{
"name": "git.beyondtheuniverse.superviber.com.",
"type": "A",
"ttl": 300,
"changetype": "REPLACE",
"records": [{"content": "3.218.167.115", "disabled": false}]
}
]
}'
Phase 4: Update GoDaddy Glue Records
At GoDaddy for superviber.com:
- Update glue records to point ns1 and ns2 to 3.218.167.115
- Set nameservers to ns1.superviber.com and ns2.superviber.com
Phase 5: Verify
# Test directly against PowerDNS
dig @3.218.167.115 git.beyondtheuniverse.superviber.com A
# Test via public DNS (after propagation)
dig git.beyondtheuniverse.superviber.com A
File Changes
| File | Change | Status |
|---|---|---|
hearth/terraform/minimal/main.tf |
Add DNS ports 53 UDP/TCP to security group | Done |
hearth/scripts/deploy-powerdns.sh |
Standalone PowerDNS deployment script | Done |
hearth/docs/rfcs/0003-powerdns-self-hosted.md |
This RFC | Done |
Rollback Plan
If PowerDNS fails:
- Update GoDaddy to use its default nameservers (restore from backup)
- Or: Use Cloudflare (already partially configured)
The Elastic IP remains stable, so only NS records need updating.
Test Plan
- PowerDNS pod running in k3s
- Port 53 UDP/TCP accessible from internet
dig @3.218.167.115 superviber.com NSreturns ns1/ns2dig @3.218.167.115 git.beyondtheuniverse.superviber.com Areturns 3.218.167.115- GoDaddy glue records updated for all 5 domains (2026-01-24)
dig @8.8.8.8 git.beyondtheuniverse.superviber.com Aresolves to 3.218.167.115- Forgejo accessible at https://git.beyondtheuniverse.superviber.com with valid TLS
Future Work
- HA DNS - Deploy on separate instance with its own EIP for redundancy
- DNSSEC - Sign zones for security
- Backup zones - Export zone files to S3 with daily backups
- DNS UI - PowerDNS-Admin web interface for zone management
- Integration with Blue - Manage DNS records as code via Blue