PowerDNS Master-Slave Deployment Using Docker

One of my latest projects was a fully dockerized name server infrastructure based on PowerDNS: one master and two slaves — one in the same subnet and the second running in a cloud on a basic virtual machine.

Why PowerDNS? Because I needed an API, a proper admin-friendly web interface, user management, and LDAP integration. PowerDNS fully matched my requirements.

Why Docker? Because I wanted some level of automation and IaC — Docker fits nicely in the middle, where you have several configs and docker-compose files fully describing the container configuration.

Domain migration tools

These are the command-line tools I created for domain migration, written in Bash: powerdns-api

This script performs the main actions needed to migrate from almost any name server (by reading zones via AXFR) into PowerDNS. You need both pdns-master and pdns-admin, because the PowerDNS API alone does not provide all the required functionality.

Then you just use the command line to migrate domains:

cat domains.txt | awk '{print "./pdns-migrator.sh --create-slave-zone="$1}'
cat domains.txt | awk '{print "./pdns-migrator.sh --axfr-retrieve="$1}'
cat domains.txt | awk '{print "./pdns-migrator.sh --convert-to-master="$1}'
cat domains.txt | awk '{print "./pdns-migrator.sh --replace-ns-soa="$1}'

Docker Compose startup

I prefer using a dedicated Systemd service to manage the Docker lifecycle. A key feature of my setup is the use of rm -fv. This ensures a "clean slate" by removing all container-specific data and anonymous volumes, preventing configuration drift and forcing all changes to be managed via IaC (docker-compose files).

Warning: This approach is destructive by design. Ensure your database and persistent data are mapped to host paths or named volumes if you need them to survive a restart.

# cat docker-compose.service 
#
# /etc/systemd/system/docker-compose.service
#

[Unit]
Description=Docker compose start service
Requires=docker.service network-online.target
After=docker.service network-online.target

[Service]
PIDFile=/run/docker-compose.pid

WorkingDirectory=/srv/docker-compose

ExecStartPre=/usr/bin/docker compose -f docker-compose.yaml down -v
ExecStartPre=/usr/bin/docker compose -f docker-compose.yaml rm -fv

ExecStart=/usr/bin/docker compose -f docker-compose.yaml up
ExecStop=/usr/bin/docker compose down -v

StandardOutput=null
StandardError=null

[Install]
WantedBy=multi-user.target

Example of master name server

# cat master-docker-compose.yaml

x-all-defaults: &all-defaults
  logging:
    driver: fluentd

services:

  fluentd:
    image: fluent/fluentd:v1.18-debian-1
    hostname: fluentd
    container_name: fluentd
    networks:
      - pdns
    ports:
      - '127.0.0.1:24224:24224'
      - '127.0.0.1:24224:24224/udp'
    volumes:
      - 'logs:/fluentd/log'

  db:
    << : *all-defaults
    image: mariadb:11.7.2-ubi
    hostname: db
    container_name: db
    networks:
      pdns:
        ipv4_address: 172.6.0.20
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - db:/var/lib/mysql:Z
    environment:
      - MARIADB_ROOT_PASSWORD=PASSWORD
    depends_on:
      - fluentd
    healthcheck:
      test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
      timeout: 10s
      retries: 5

  pdns-master:
    << : *all-defaults
    image: pschiffe/pdns-mysql:4.9
    hostname: ns1.domain.com
    container_name: pdns-master
    networks:
      pdns:
        ipv4_address: 172.6.0.30
    ports:
      - '53:53'
      - '53:53/udp'
      - '8987:8987'
    extra_hosts:
      - 'ns1.domain.com:111.111.111.111'
      - 'ns2.domain.com:10.0.0.10'
      - 'ns3.domain.com:10.0.100.55'
    volumes:
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PDNS_gmysql_host=172.6.0.20
      - PDNS_gmysql_password=PASSWORD
      - PDNS_primary=yes
      - PDNS_api=yes
      - PDNS_api_key=PASSWORD
      - PDNS_webserver=yes
      - PDNS_webserver_password=PASSWORD
      - PDNS_webserver_address=0.0.0.0
      - PDNS_webserver_port=8987
      - PDNS_webserver_allow_from=1.2.3.4/32
      - PDNS_version_string=anonymous
      - PDNS_default_ttl=1500
      - PDNS_allow_axfr_ips=10.0.0.10/32,111.111.111.111/32
      - PDNS_allow_notify_from=10.0.0.10/32,111.111.111.111/32
      - PDNS_only_notify=10.0.0.10/32,111.111.111.111/32
      - PDNS_also_notify=10.0.0.10/32,111.111.111.111/32
      - PDNS_default_soa_content=ns1.domain.com. hostmaster.@ 0 3600 3600 1209600 86400
      - PDNS_log_dns_details=yes
      - PDNS_loglevel=4
    depends_on:
      - fluentd
      - db

Example of slave name server

# cat slave-docker-compose.yaml

x-all-defaults: &all-defaults
  logging:
    driver: fluentd

services:

  pdns-slave:
    << : *all-defaults
    image: pschiffe/pdns-mysql:4.9
    hostname: ns2.domain.com
    container_name: pdns-slave
    networks:
      pdns:
        ipv4_address: 172.6.0.30
    ports:
      - '53:53'
      - '53:53/udp'
      - '8987:8987'
    extra_hosts:
      - 'ns1.domain.com:111.111.111.111'
      - 'ns2.domain.com:10.0.0.10'
      - 'ns3.domain.com:10.0.100.55'
    volumes:
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PDNS_gmysql_host=172.6.0.20
      - PDNS_gmysql_dbname=powerdnsslave
      - PDNS_gmysql_password=PASSWORD
      - PDNS_secondary=yes
      - PDNS_autosecondary=yes
      - PDNS_webserver=yes
      - PDNS_webserver_password=PASSWORD
      - PDNS_webserver_address=0.0.0.0
      - PDNS_webserver_port=8987
      - PDNS_webserver_allow_from=1.2.3.4/32
      - PDNS_version_string=anonymous
      - PDNS_disable_axfr=yes
      - PDNS_allow_notify_from=10.0.0.10/32,111.111.111.111/32
      - PDNS_log_dns_details=yes
      - PDNS_loglevel=4
      - SUPERMASTER_IPS=10.0.0.123
      - SUPERMASTER_HOSTS=ns1.domain.com
    depends_on:
      - fluentd
      - db

For more details on network security, check out my guide on how to deal with firewall configuration on Docker hosts: Firewall Control on Docker Hosts Using the DOCKER-USER iptables Chain.

Easy as 1-2-3: Build → Migrate → Use 😉

Human Logic, AI Syntax... Note on Content: I'm a Systems Engineer, not a native English writer. To ensure my technical ideas are clear and accessible, I use AI tools to polish the grammar and style. The workflow is simple: I provide the logic, the code, and the real-world experience. The AI handles the "English-to-Human" translation layer. If you find a bug, that's on me. If you find a perfectly placed comma, that's probably the AI.

Comments

Popular posts from this blog

FreeRadius with Google Workspace LDAP

Fixing pssh (parallel-ssh) Problems on Debian 10 with Python 3.7