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 😉
Comments
Post a Comment