Self-Hosting PowerDNS with Docker Compose
What Is PowerDNS?
Want to run your own authoritative DNS server — the kind that actually answers “where is example.com?” rather than just forwarding the question? PowerDNS is the open-source standard for that job. It’s used by ISPs, hosting providers, and enterprises to serve billions of DNS queries daily, and its Docker packaging makes it accessible for homelabs too.
PowerDNS consists of two separate components: the Authoritative Server (serves zones you control) and the Recursor (resolves queries recursively). This guide covers the Authoritative Server with a MariaDB backend and the PowerDNS Admin web interface for zone management.
Quick Verdict
PowerDNS is overkill if you just want local DNS resolution or ad blocking — use Unbound or Pi-hole for those. But if you’re hosting domains, managing DNS zones with an API, or need DNSSEC signing, PowerDNS is the most capable self-hosted option. The web admin interface makes zone management accessible without touching SQL.
Use Cases
| Use Case | PowerDNS Fit |
|---|---|
| Hosting your own domain’s DNS | Excellent — designed for this |
| Internal DNS for homelab | Good — but heavier than needed (try CoreDNS) |
| DNS-level ad blocking | Wrong tool — use Pi-hole or AdGuard Home |
| Recursive resolution | Use PowerDNS Recursor or Unbound instead |
| Multi-domain DNS hosting | Excellent — scales to thousands of zones |
| DNSSEC signing | Built-in, automated |
| DNS API integration | Full REST API, used by many hosting panels |
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 512 MB of free RAM (PowerDNS + MariaDB + Admin)
- Port 53 available on the host
- A domain name (optional, for the admin interface)
Docker Compose Configuration
Create a directory for PowerDNS:
mkdir -p /opt/powerdns && cd /opt/powerdns
Create a docker-compose.yml:
services:
mariadb:
image: mariadb:11.7
container_name: pdns-mariadb
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} # Change in .env
volumes:
- mariadb-data:/var/lib/mysql
networks:
pdns:
aliases:
- mysql
- db
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 10s
retries: 5
restart: unless-stopped
powerdns:
image: pschiffe/pdns-mysql:5.0
container_name: powerdns
hostname: ns1.example.com # Change to your hostname
ports:
- "53:53/tcp"
- "53:53/udp"
- "8081:8081" # API/webserver
environment:
PDNS_gmysql_password: ${DB_ROOT_PASSWORD}
PDNS_primary: "yes"
PDNS_api: "yes"
PDNS_api_key: ${PDNS_API_KEY} # Change in .env
PDNS_webserver: "yes"
PDNS_webserver_address: "0.0.0.0"
PDNS_webserver_password: ${PDNS_WEBSERVER_PASSWORD} # Change in .env
PDNS_webserver_allow_from: "172.20.0.0/16,127.0.0.1"
PDNS_version_string: "anonymous"
PDNS_default_ttl: "3600"
PDNS_launch: "gmysql"
networks:
pdns:
depends_on:
mariadb:
condition: service_healthy
healthcheck:
test: ["CMD", "pdns_control", "ping"]
interval: 10s
timeout: 10s
retries: 3
start_period: 5s
restart: unless-stopped
pdns-admin:
image: pschiffe/pdns-admin:0.4.1
container_name: pdns-admin
ports:
- "8989:8080" # Admin web UI
environment:
PDNS_ADMIN_SQLA_DB_TYPE: "mysql"
PDNS_ADMIN_SQLA_DB_HOST: "mysql"
PDNS_ADMIN_SQLA_DB_PORT: "3306"
PDNS_ADMIN_SQLA_DB_USER: "root"
PDNS_ADMIN_SQLA_DB_PASSWORD: ${DB_ROOT_PASSWORD}
PDNS_ADMIN_SQLA_DB_NAME: "pdnsadmin"
PDNS_API_URL: "http://powerdns:8081/"
PDNS_API_KEY: ${PDNS_API_KEY}
PDNS_VERSION: "5.0"
volumes:
- pdns-admin-upload:/opt/powerdns-admin/upload
networks:
pdns:
depends_on:
mariadb:
condition: service_healthy
powerdns:
condition: service_healthy
restart: unless-stopped
networks:
pdns:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
volumes:
mariadb-data:
pdns-admin-upload:
Create the .env file alongside:
cat > /opt/powerdns/.env << 'EOF'
# CHANGE ALL OF THESE VALUES
DB_ROOT_PASSWORD=change-this-strong-password
PDNS_API_KEY=change-this-api-secret-key
PDNS_WEBSERVER_PASSWORD=change-this-webserver-password
EOF
Important: Replace all three values with strong, unique passwords.
Start the stack:
docker compose up -d
Initial Setup
1. Access PowerDNS Admin
Open http://your-server-ip:8989 in your browser. Create an admin account on first visit — there are no default credentials.
2. Connect to PowerDNS Server
In PowerDNS Admin, navigate to Settings and verify the API connection:
- API URL:
http://powerdns:8081/ - API Key: the value you set in
.env
3. Create Your First Zone
- Click New Domain
- Enter your domain name (e.g.,
example.com) - Select Master as the domain type
- Click Create
- Add DNS records (A, AAAA, CNAME, MX, TXT, etc.) through the web interface
4. Verify DNS Resolution
# Query PowerDNS directly for a record you created
dig @127.0.0.1 example.com A
# Test the API
curl -H "X-API-Key: your-api-key" http://localhost:8081/api/v1/servers/localhost/zones
Configuration
Environment Variables
PowerDNS uses a naming convention for environment variables: any variable starting with PDNS_ is converted to a config directive:
PDNS_prefix is stripped- Underscores become hyphens
- Example:
PDNS_gmysql_host=mysql→gmysql-host=mysql
| Variable | Default | Purpose |
|---|---|---|
PDNS_gmysql_host | mysql | MariaDB hostname |
PDNS_gmysql_password | — | Database password (required) |
PDNS_gmysql_dbname | powerdns | Database name |
PDNS_primary | no | Enable primary (master) mode |
PDNS_secondary | no | Enable secondary (slave) mode |
PDNS_api | no | Enable REST API |
PDNS_api_key | — | API authentication key |
PDNS_webserver | no | Enable stats webserver |
PDNS_webserver_address | 127.0.0.1 | Webserver listen address |
PDNS_webserver_allow_from | — | IP ranges allowed to access API |
PDNS_dnssec | no | Enable DNSSEC signing |
PDNS_default_ttl | 3600 | Default TTL for new records |
PDNS_version_string | — | Override version in DNS responses |
PDNS_launch | — | Backend to load (e.g., gmysql) |
Enable DNSSEC
Add to the PowerDNS service environment:
environment:
PDNS_dnssec: "yes"
Then sign a zone via the API:
curl -X PUT -H "X-API-Key: your-api-key" \
http://localhost:8081/api/v1/servers/localhost/zones/example.com./cryptokeys \
-d '{"keytype":"ksk","active":true}'
Or enable DNSSEC per-zone through PowerDNS Admin’s web interface.
Advanced Configuration
Primary/Secondary Replication
For DNS redundancy, run a secondary PowerDNS instance that replicates zones from the primary:
Primary additions:
environment:
PDNS_allow_axfr_ips: "10.0.0.21" # Secondary server IP
PDNS_only_notify: "10.0.0.21"
Secondary setup:
environment:
PDNS_secondary: "yes"
PDNS_autosecondary: "yes"
PDNS_allow_notify_from: "10.0.0.20" # Primary server IP
PostgreSQL Backend
Replace MariaDB with PostgreSQL by using pschiffe/pdns-pgsql:5.0:
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 10s
retries: 5
restart: unless-stopped
powerdns:
image: pschiffe/pdns-pgsql:5.0
environment:
PDNS_gpgsql_password: ${DB_ROOT_PASSWORD}
PDNS_launch: "gpgsql"
# ... rest of config same as MySQL variant
API Usage Examples
PowerDNS has a full REST API for programmatic zone management:
# List all zones
curl -H "X-API-Key: your-key" http://localhost:8081/api/v1/servers/localhost/zones
# Create a zone
curl -X POST -H "X-API-Key: your-key" \
-H "Content-Type: application/json" \
http://localhost:8081/api/v1/servers/localhost/zones \
-d '{"name":"newdomain.com.","kind":"Master","nameservers":["ns1.example.com."]}'
# Add a record
curl -X PATCH -H "X-API-Key: your-key" \
-H "Content-Type: application/json" \
http://localhost:8081/api/v1/servers/localhost/zones/newdomain.com. \
-d '{"rrsets":[{"name":"www.newdomain.com.","type":"A","ttl":3600,"changetype":"REPLACE","records":[{"content":"1.2.3.4","disabled":false}]}]}'
Reverse Proxy
To expose PowerDNS Admin behind a reverse proxy for HTTPS access:
Nginx Proxy Manager: Create a proxy host pointing to your server’s IP on port 8989. See Reverse Proxy Setup.
Caddy:
pdns.yourdomain.com {
reverse_proxy localhost:8989
}
The DNS service itself (port 53) doesn’t go through a reverse proxy. For remote DNS access, use a VPN (Tailscale, WireGuard) or Cloudflare Tunnel.
Backup
Critical Data
| Component | Location | Backup Method |
|---|---|---|
| DNS zones & records | MariaDB powerdns database | mysqldump |
| Admin users & settings | MariaDB pdnsadmin database | mysqldump |
| Admin uploads | pdns-admin-upload volume | File copy |
| Configuration | .env file | File copy |
| DNSSEC keys | Stored in database | Included in mysqldump |
Backup Script
#!/bin/bash
BACKUP_DIR="/opt/backups/powerdns/$(date +%F)"
mkdir -p "$BACKUP_DIR"
# Dump both databases
docker exec pdns-mariadb mysqldump -u root -p"$DB_ROOT_PASSWORD" powerdns > "$BACKUP_DIR/powerdns.sql"
docker exec pdns-mariadb mysqldump -u root -p"$DB_ROOT_PASSWORD" pdnsadmin > "$BACKUP_DIR/pdnsadmin.sql"
# Copy config
cp /opt/powerdns/.env "$BACKUP_DIR/"
cp /opt/powerdns/docker-compose.yml "$BACKUP_DIR/"
echo "Backup complete: $BACKUP_DIR"
See Backup Strategy and Backing Up Docker Volumes for comprehensive approaches.
Troubleshooting
PowerDNS Can’t Connect to Database
Symptom: PowerDNS container restarts repeatedly, logs show Connection refused or Access denied
Fix: Ensure MariaDB is healthy before PowerDNS starts. The depends_on: condition: service_healthy in the compose file handles this. Verify the password in .env matches between all services.
API Returns 401 Unauthorized
Symptom: API calls fail with Not authorized
Fix: Verify the X-API-Key header matches the PDNS_api_key environment variable exactly. Check PDNS_webserver_allow_from includes your client IP.
Zone Changes Not Propagating
Symptom: Updated records return old values
Fix: Check TTL values — clients cache records for the TTL duration. Force a cache flush on the client side:
# Linux
sudo systemd-resolve --flush-caches
# macOS
sudo dscacheutil -flushcache
PowerDNS Admin Can’t Reach API
Symptom: Admin UI shows “Unable to connect to PowerDNS API”
Fix: Verify PDNS_API_URL uses the Docker service name (e.g., http://powerdns:8081/), not localhost. Both containers must be on the same Docker network.
Port 53 Already in Use
Symptom: bind: address already in use
Fix: Disable systemd-resolved:
sudo systemctl disable --now systemd-resolved
Resource Requirements
| Resource | Value |
|---|---|
| RAM | ~200 MB (PowerDNS ~50 MB + MariaDB ~100 MB + Admin ~50 MB) |
| CPU | Low — handles thousands of queries/second on modest hardware |
| Disk | ~500 MB for containers + database grows with zone count |
| Network | Minimal bandwidth; inbound DNS on port 53 |
Verdict
PowerDNS is the right choice when you need authoritative DNS hosting with a web interface and API. The pschiffe/pdns-mysql Docker images make deployment straightforward, and the Admin panel means you don’t need to manage zone files or SQL directly. DNSSEC signing is built in and automated.
For simple homelab DNS resolution, PowerDNS is more infrastructure than you need — use Unbound for recursive resolution or CoreDNS for lightweight zone serving. For DNS-level ad blocking, use Pi-hole or AdGuard Home. PowerDNS shines when you’re hosting actual domain DNS and need the reliability and API access that ISPs and hosting providers depend on.
FAQ
What’s the difference between PowerDNS Authoritative and Recursor?
The Authoritative Server serves zones you control (like running your own nameserver for example.com). The Recursor resolves queries by walking the DNS hierarchy (like Unbound). They’re separate products. This guide covers the Authoritative Server.
Can I use PowerDNS for local homelab DNS?
Yes, but it’s heavier than necessary. CoreDNS or Unbound with local zones are simpler for homelab DNS. PowerDNS makes sense when you need the web admin interface, API, or DNSSEC.
Is PowerDNS Admin required?
No — PowerDNS works without the admin interface. You can manage zones entirely through the REST API or by inserting records directly into the database. The admin interface just makes it easier.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments