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 CasePowerDNS Fit
Hosting your own domain’s DNSExcellent — designed for this
Internal DNS for homelabGood — but heavier than needed (try CoreDNS)
DNS-level ad blockingWrong tool — use Pi-hole or AdGuard Home
Recursive resolutionUse PowerDNS Recursor or Unbound instead
Multi-domain DNS hostingExcellent — scales to thousands of zones
DNSSEC signingBuilt-in, automated
DNS API integrationFull 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

  1. Click New Domain
  2. Enter your domain name (e.g., example.com)
  3. Select Master as the domain type
  4. Click Create
  5. 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=mysqlgmysql-host=mysql
VariableDefaultPurpose
PDNS_gmysql_hostmysqlMariaDB hostname
PDNS_gmysql_passwordDatabase password (required)
PDNS_gmysql_dbnamepowerdnsDatabase name
PDNS_primarynoEnable primary (master) mode
PDNS_secondarynoEnable secondary (slave) mode
PDNS_apinoEnable REST API
PDNS_api_keyAPI authentication key
PDNS_webservernoEnable stats webserver
PDNS_webserver_address127.0.0.1Webserver listen address
PDNS_webserver_allow_fromIP ranges allowed to access API
PDNS_dnssecnoEnable DNSSEC signing
PDNS_default_ttl3600Default TTL for new records
PDNS_version_stringOverride version in DNS responses
PDNS_launchBackend 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

ComponentLocationBackup Method
DNS zones & recordsMariaDB powerdns databasemysqldump
Admin users & settingsMariaDB pdnsadmin databasemysqldump
Admin uploadspdns-admin-upload volumeFile copy
Configuration.env fileFile copy
DNSSEC keysStored in databaseIncluded 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

ResourceValue
RAM~200 MB (PowerDNS ~50 MB + MariaDB ~100 MB + Admin ~50 MB)
CPULow — handles thousands of queries/second on modest hardware
Disk~500 MB for containers + database grows with zone count
NetworkMinimal 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.

Comments