Databases

Install PowerDNS and PowerDNS-Admin on Debian 13 / 12

Debian 13 ships PowerDNS Authoritative 4.9, a release train now at the end of its upstream support window, while the current PowerDNS-Admin release is tested only against 5.0 and 5.1. The supported path for PowerDNS on Debian is therefore the official PowerDNS repository, which installs 5.1 on Debian 13 and Debian 12 alike and upgraded an existing 4.9 install on the test box in about four seconds without a schema change.

Original content from computingforgeeks.com - post 114901

Benchmarked October 2026 on PowerDNS Authoritative 5.1 (Debian 13 primary, Debian 12 secondary).

The build below puts the PowerDNS primary with a MariaDB backend and the PowerDNS-Admin web interface on Debian 13, then adds a second PowerDNS server on Debian 12 that provisions itself from the primary through NOTIFY and AXFR. Every number in it was measured on the two test servers: query throughput with and without the packet cache, the delay between an edit and the secondary serving it, and the memory each component holds. It also covers the in-place upgrade for anyone still running the 4.9 package from the Debian archive.

How the two servers fit together

All edits land on the primary, either through PowerDNS-Admin or the HTTP API. The secondary holds a read-only copy in SQLite and pulls each change after the primary sends a NOTIFY. Resolvers query both servers on port 53:

PowerDNS primary and secondary architecture diagram on Debian: on bench01 10.0.1.51, PowerDNS-Admin calls the pdns primary API on 8081 and reaches MariaDB over the Unix socket, the primary reads MariaDB on TCP 127.0.0.1:3306, and bench02 10.0.1.52 receives NOTIFY and pulls the zone by AXFR on port 53; resolvers query both servers

The test pair: bench01 at 10.0.1.51 runs Debian 13 with the primary, MariaDB, Docker and Nginx; bench02 at 10.0.1.52 runs Debian 12 with only the secondary.

Prerequisites and sizing

Both test servers had 2 vCPU; the primary had 3 GB of RAM and held about 953 MB in use with everything running: PowerDNS-Admin 377 MiB, MariaDB 162 MiB, pdns_server 66 MiB. The secondary had 2 GB and used 314 MB in total. Treat those as a floor for following along, not a production recommendation. The numbers that should size a production server are query rate and cache hit ratio: with the packet cache on, the 2 vCPU primary answered 141,708 queries per second, and with every cache off it managed 5,576, so CPU follows uncached query volume while RAM follows the MariaDB buffer pool, which should hold the records table. The test held a single zone, so zone count was not measured. Give the primary and every secondary the same CPU, because a resolver may pick any of them.

The rest of the list: root or sudo on both servers, UDP and TCP port 53 reachable from clients, TCP 53 open from the secondary to the primary for zone transfers, and a hostname for the web interface (dns.example.com here) with an A record pointing at the primary and port 80 open for the certificate challenge. New Debian 13 servers benefit from the Debian 13 post-install steps first.

Step 1: Set reusable shell variables

Database credentials, the API key, the zone name and both server addresses repeat throughout. Export them on the primary:

export PDNS_DB="pdns"
export PDNS_USER="pdnsuser"
export PDNS_PASS="ChangeMe-Pdns-2026"
export PDA_DB="pdnsadmin"
export PDA_USER="pdnsadmin"
export PDA_PASS="ChangeMe-Pda-2026"
export API_KEY="$(openssl rand -hex 16)"
export ZONE="example.internal"
export PRIMARY_IP="10.0.1.51"
export SECONDARY_IP="10.0.1.52"
export PDA_DOMAIN="dns.example.com"
export ADMIN_EMAIL="[email protected]"

Replace both passwords and the two addresses with real values. Keep both passwords to letters, digits, - and _: one is written into a config file with sed, and the other ends up inside a database URI where characters such as @, :, /, % and # break the parsing. The variables last for the current shell only, so print them once and store the API key, because a new shell would generate a different one:

echo "zone ${ZONE} | primary ${PRIMARY_IP} | secondary ${SECONDARY_IP} | API key ${API_KEY}"

Step 2: Free port 53

Debian cloud images, and any install that enabled systemd-resolved, run its stub listener on 127.0.0.53 and 127.0.0.54. Installing PowerDNS while the stub holds port 53 fails at the first start; on the Debian 13 test box the package install printed Job for pdns.service failed because the control process exited with error code. and the journal showed the cause:

Unable to bind UDP socket to '0.0.0.0:53': Address already in use
Fatal error: Unable to bind to UDP socket

Check what owns the port before installing anything. An empty result means systemd-resolved is not running and this step can be skipped:

sudo ss -lntup 'sport = :53'

If systemd-resolve appears, disable the stub, point /etc/resolv.conf at the upstream resolvers so the host can still look up names, and restart the service:

sudo sed -i 's/^#\?DNSStubListener=.*/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved

Run the same step on the secondary later; the Debian 12 test server had the stub enabled as well.

Step 3: Install MariaDB and create the PowerDNS database

The Debian 13 archive provides MariaDB 11.8, which PowerDNS supports without changes. The MariaDB on Debian guide covers tuning beyond what is needed here:

sudo apt update
sudo apt install -y mariadb-server

Create the database and a user restricted to localhost:

sudo mariadb -e "CREATE DATABASE ${PDNS_DB} DEFAULT CHARACTER SET utf8mb4;"
sudo mariadb -e "CREATE USER '${PDNS_USER}'@'localhost' IDENTIFIED BY '${PDNS_PASS}';"
sudo mariadb -e "GRANT ALL ON ${PDNS_DB}.* TO '${PDNS_USER}'@'localhost'; FLUSH PRIVILEGES;"

Step 4: Install PowerDNS 5.1 from the official repository

The versions available on the test servers, read from apt-cache policy pdns-server:

SourceDebian 13 (trixie)Debian 12 (bookworm)
Debian archive4.9.17 (upstream end of life ~September 2026)4.7.3 (upstream end of life August 2025)
repo.powerdns.com auth-515.1.45.1.4
Tested by current PowerDNS-AdminRepository onlyRepository only

Upstream dates come from the PowerDNS end-of-life table. The Debian security team still patches its own 4.9 build, so the archive package is not abandoned, but it sits outside the matrix PowerDNS-Admin tests against. In practice the 2026.08.1 interface still listed the zone from a 4.9.17 server on the test box; nothing guarantees that for edits or future releases. Add the repository key, the source and an apt pin so the pdns-* packages come from repo.powerdns.com:

sudo install -d /etc/apt/keyrings
curl -fsSL https://repo.powerdns.com/FD380FBB-pub.asc | sudo tee /etc/apt/keyrings/auth-51-pub.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/auth-51-pub.asc] http://repo.powerdns.com/debian $(. /etc/os-release && echo "$VERSION_CODENAME")-auth-51 main" | sudo tee /etc/apt/sources.list.d/pdns.list
printf 'Package: pdns-*\nPin: origin repo.powerdns.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/auth-51

The codename expands to trixie on Debian 13 and bookworm on Debian 12, so the same block works on both servers. Refresh the lists and install the server with the MySQL backend:

sudo apt update
sudo apt install -y pdns-server pdns-backend-mysql

Confirm that the running daemon is the repository build:

sudo pdns_control version

The output is the bare version string, 5.1.4 on both test servers.

Step 5: Load the schema and configure the primary

Import the gmysql schema shipped with the backend package, then remove the default BIND-file backend so PowerDNS reads only MariaDB:

sudo mariadb "${PDNS_DB}" < /usr/share/pdns-backend-mysql/schema/schema.mysql.sql
sudo rm -f /etc/powerdns/pdns.d/bind.conf

Open a local settings file, which the package include directory loads after the main pdns.conf:

sudo vim /etc/powerdns/pdns.d/pdns.local.conf

Paste the configuration below, leaving the uppercase placeholders in place because the next command fills them from the shell variables:

launch+=gmysql
gmysql-host=127.0.0.1
gmysql-port=3306
gmysql-dbname=PDNS_DB_HERE
gmysql-user=PDNS_USER_HERE
gmysql-password=PDNS_PASS_HERE
gmysql-dnssec=yes

default-soa-content=ns1.@ hostmaster.@ 0 10800 3600 604800 3600

api=yes
api-key=API_KEY_HERE
webserver=yes
webserver-address=0.0.0.0
webserver-port=8081
webserver-allow-from=127.0.0.1,::1,172.17.0.0/16

# Zone transfers to the secondary
primary=yes
allow-axfr-ips=127.0.0.1,SECONDARY_IP_HERE/32

default-soa-content replaces the 5.1 default primary name, a.misconfigured.dns.server.invalid, with the ns1 name of the zone itself. The API listens on all addresses because PowerDNS-Admin reaches it from the Docker bridge at 172.17.0.1; webserver-allow-from and the firewall in Step 8 confine it. primary=yes makes the server send NOTIFY messages, and allow-axfr-ips limits zone transfers to the secondary. Substitute the values, set ownership and restart:

sudo sed -i -e "s/PDNS_DB_HERE/${PDNS_DB}/" -e "s/PDNS_USER_HERE/${PDNS_USER}/" -e "s/PDNS_PASS_HERE/${PDNS_PASS}/" -e "s/API_KEY_HERE/${API_KEY}/" -e "s/SECONDARY_IP_HERE/${SECONDARY_IP}/" /etc/powerdns/pdns.d/pdns.local.conf
sudo chown root:pdns /etc/powerdns/pdns.d/pdns.local.conf
sudo chmod 640 /etc/powerdns/pdns.d/pdns.local.conf
sudo systemctl restart pdns
systemctl is-active pdns

The last command must print active. Any other result means a typo in the file, and sudo journalctl -u pdns -n 20 names the offending line.

Step 6: Create the zone and verify resolution

PowerDNS 5 renamed the pdnsutil commands to an object-verb form and made record names absolute. Every record below therefore carries the full name. The zone gets two NS records from the start, because the secondary in Step 11 only accepts a NOTIFY for a zone that lists it as a name server:

sudo pdnsutil zone create "${ZONE}" "ns1.${ZONE}"
sudo pdnsutil zone set-kind "${ZONE}" primary
sudo pdnsutil rrset replace "${ZONE}" "${ZONE}" NS 3600 "ns1.${ZONE}." "ns2.${ZONE}."
sudo pdnsutil rrset add "${ZONE}" "ns1.${ZONE}" A 3600 "${PRIMARY_IP}"
sudo pdnsutil rrset add "${ZONE}" "ns2.${ZONE}" A 3600 "${SECONDARY_IP}"
sudo pdnsutil rrset add "${ZONE}" "www.${ZONE}" A 3600 10.0.1.80
sudo pdnsutil metadata set "${ZONE}" SOA-EDIT-API DEFAULT
sudo pdnsutil zone increase-serial "${ZONE}"
sudo pdns_control rediscover

SOA-EDIT-API DEFAULT is already what PowerDNS 5 sets on new zones; the explicit line keeps the guide correct for zones created before the upgrade. It makes every API or PowerDNS-Admin edit raise the serial, which is how the secondary learns that something changed. rediscover loads the new zone into the zone cache of the running server; skipping it leaves the zone answering REFUSED for up to 300 seconds. Query the primary directly on the loopback address, installing dig first if the server does not have it:

sudo apt install -y bind9-dnsutils
dig @127.0.0.1 "www.${ZONE}" +short
dig @127.0.0.1 "${ZONE}" NS +short

Both name servers are listed and the A record resolves:

10.0.1.80
ns1.example.internal.
ns2.example.internal.

Step 7: Deploy PowerDNS-Admin with MariaDB over the Unix socket

PowerDNS-Admin keeps its users, roles and audit history in its own database. Placing it in MariaDB next to the zone data means one backup covers both. Create the database with a localhost-only user:

sudo mariadb -e "CREATE DATABASE ${PDA_DB} DEFAULT CHARACTER SET utf8mb4;"
sudo mariadb -e "CREATE USER '${PDA_USER}'@'localhost' IDENTIFIED BY '${PDA_PASS}';"
sudo mariadb -e "GRANT ALL ON ${PDA_DB}.* TO '${PDA_USER}'@'localhost'; FLUSH PRIVILEGES;"

Older instructions, including the previous version of this guide, widened the MariaDB bind-address to 0.0.0.0 so the container could connect over TCP. Mounting the MariaDB socket directory into the container avoids that entirely: port 3306 stays on loopback, and the localhost user works unchanged. Install Docker from the Debian archive (the Docker on Debian guide covers the upstream Docker CE packages if you prefer them) and start the container:

sudo apt install -y docker.io
sudo docker run -d --name powerdns-admin --restart unless-stopped \
  -p 127.0.0.1:9191:80 \
  --add-host host.docker.internal:host-gateway \
  -v /run/mysqld:/run/mysqld \
  -e SECRET_KEY="$(openssl rand -hex 32)" \
  -e SQLALCHEMY_DATABASE_URI="mysql://${PDA_USER}:${PDA_PASS}@localhost/${PDA_DB}?unix_socket=/run/mysqld/mysqld.sock" \
  powerdnsadmin/pda-legacy:latest

The image is named pda-legacy for historical reasons; the project states it is the maintained build, and the version tested here was 2026.08.1. The first start runs the schema migrations against pdnsadmin. Verify the container and the tables it created:

curl -s -o /dev/null -w "login HTTP %{http_code}\n" http://127.0.0.1:9191/login
sudo mariadb "${PDA_DB}" -e "SHOW TABLES;" | head -5

An HTTP 200 and a populated table list confirm the socket connection works:

login HTTP 200
Tables_in_pdnsadmin
account
account_user
alembic_version
apikey

Step 8: Open the firewall

Debian does not install a firewall frontend by default, and UFW on top of nftables is enough for a single host:

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 53/udp
sudo ufw allow 53/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow in on docker0 to any port 8081 proto tcp
sudo ufw --force enable

OpenSSH goes in before enable so the session survives; if sshd uses a non-standard port, allow that port instead. The docker0 rule matters: packets from a container to a host service traverse the INPUT chain of the host, so the default deny policy would drop every API call from PowerDNS-Admin while the same curl from the host keeps working. The rule matches only the bridge interface, and a connection test to port 8081 from another machine timed out on the test network.

Step 9: Put Nginx and a Let’s Encrypt certificate in front

Install Nginx with the certbot plugin and remove the default site. On Debian, nginx lives in /usr/sbin, so version checks need sudo nginx -v:

sudo apt install -y nginx certbot python3-certbot-nginx
sudo rm -f /etc/nginx/sites-enabled/default

Create the proxy site that forwards the public hostname to the container on loopback:

sudo vim /etc/nginx/sites-available/powerdns-admin

The HTTP server block below is all certbot needs; it adds the TLS listener itself:

server {
    listen 80;
    listen [::]:80;
    server_name PDA_DOMAIN_HERE;

    location / {
        proxy_pass http://127.0.0.1:9191;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Fill in the hostname, enable the site and request the certificate through the HTTP-01 challenge:

sudo sed -i "s/PDA_DOMAIN_HERE/${PDA_DOMAIN}/" /etc/nginx/sites-available/powerdns-admin
sudo ln -s /etc/nginx/sites-available/powerdns-admin /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d "${PDA_DOMAIN}" --non-interactive --agree-tos --redirect -m "${ADMIN_EMAIL}"

The test servers had no public address, so this certbot command ran against a local Pebble ACME server with an added --server flag. Certificate issuance and the Nginx rewrite completed the same way a public run does:

Successfully received certificate.
Successfully deployed certificate for dns.example.com to /etc/nginx/sites-enabled/powerdns-admin

Alternative: DNS-01 for a server without public port 80

An admin interface on a private network cannot answer the HTTP-01 challenge. DNS-01 proves the name with a TXT record instead, using the certbot plugin for the DNS provider that hosts the domain:

ProviderDebian package
Cloudflarepython3-certbot-dns-cloudflare
Amazon Route 53python3-certbot-dns-route53
Google Cloud DNSpython3-certbot-dns-google
Any RFC 2136 serverpython3-certbot-dns-rfc2136

With Cloudflare as the example (substitute the plugin and credentials file for another provider), store a token with Zone DNS Edit rights, request the certificate with a reload hook, and let the Nginx installer apply it. This path was not run end to end on the test servers:

sudo apt install -y python3-certbot-dns-cloudflare
echo "dns_cloudflare_api_token = your-token-here" | sudo tee /etc/letsencrypt/cloudflare.ini >/dev/null
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d "${PDA_DOMAIN}" --non-interactive --agree-tos -m "${ADMIN_EMAIL}" --deploy-hook "systemctl reload nginx"
sudo certbot install --nginx --cert-name "${PDA_DOMAIN}" --redirect

Step 10: Configure PowerDNS-Admin

Browse to https://dns.example.com and use Create an account. The first registered account receives the Administrator role. The form includes a six-digit CAPTCHA, and a wrong answer clears both password fields. After logging in, PowerDNS-Admin opens Settings > Server: set the API URL to http://host.docker.internal:8081, paste the ${API_KEY} value and save. The Dashboard then lists the zone with the type reported as Primary. The serial is the one increase-serial set until the first edit through the interface or API, after which SOA-EDIT-API DEFAULT switches it to the date-based YYYYMMDDnn form shown here:

PowerDNS-Admin dashboard on Debian 13 listing example.internal as a Primary zone with a date-based serial

Disable self-registration next, under Settings > Authentication by clearing Allow users to sign up; otherwise anyone who reaches the page can create an account. Once it is off, the /register URL returns HTTP 500 instead of a form, which is expected in this release. Records are edited from the zone page, where changes are staged until Save Changes and Apply Changes are confirmed:

PowerDNS-Admin zone records for example.internal on Debian 13 with MX, two NS records and A records for mail, ns1, ns2 and www

Every applied change goes through the API, raises the serial through SOA-EDIT-API and queues a NOTIFY to the secondary. The Ubuntu version of this guide walks through each screen of the first-run setup and the record editor with screenshots.

Step 11: Add a secondary server on Debian 12

The secondary needs no MariaDB, because it stores transferred zones in SQLite through the gsqlite3 backend, and with autosecondary it creates each zone the first time the primary notifies it, so new zones need no work on this server. On bench02, repeat Step 2 if systemd-resolved holds port 53 and add the repository exactly as in Step 4, then install the SQLite backend:

sudo apt update
sudo apt install -y pdns-server pdns-backend-sqlite3 sqlite3 bind9-dnsutils

Create the SQLite database from the shipped schema, hand it to the pdns user and remove the BIND backend file:

sudo sqlite3 /var/lib/powerdns/pdns.sqlite3 < /usr/share/pdns-backend-sqlite3/schema/schema.sqlite3.sql
sudo chown pdns:pdns /var/lib/powerdns/pdns.sqlite3
sudo rm -f /etc/powerdns/pdns.d/bind.conf

Open the settings file on the secondary, which replaces the gmysql configuration used on the primary:

sudo vim /etc/powerdns/pdns.d/pdns.local.conf

Five settings cover the backend and the secondary role:

launch+=gsqlite3
gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
gsqlite3-dnssec=yes

secondary=yes
autosecondary=yes

Restart the service and register the primary as an autoprimary. The second argument must be a name server listed in the NS records of the zone, which is why Step 6 added ns2. Replace the address with the IP of your primary and the name with the ns2 host of your zone:

sudo chown root:pdns /etc/powerdns/pdns.d/pdns.local.conf
sudo chmod 640 /etc/powerdns/pdns.d/pdns.local.conf
sudo systemctl restart pdns
sudo pdnsutil autoprimary add 10.0.1.51 ns2.example.internal
sudo pdnsutil autoprimary list

Firewall the secondary the same way, allowing only SSH and DNS:

sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 53/udp
sudo ufw allow 53/tcp
sudo ufw --force enable

Back on the primary, trigger the first NOTIFY:

sudo pdns_control notify "${ZONE}"

The secondary answered for www.example.internal 1.31 seconds after this command, timed by polling it from a third machine with no also-notify set, so the NOTIFY went only to the name servers in the NS set. Its journal from that run records the whole sequence:

Received NOTIFY for example.internal from 10.0.1.51 for which we are not authoritative, trying autoprimary
Created new secondary zone 'example.internal' from autoprimary 10.0.1.51
AXFR-in zone 'example.internal', primary '10.0.1.51', zone committed with serial 2026100718

Confirm the zone type on bench02, noting that PowerDNS 5.1 still prints the old term in this message:

sudo pdnsutil zone show example.internal | head -3

The primary address and the last transfer time are listed:

This is a Slave zone
Primary: 10.0.1.51:53
Last time we got update from primary: Wed 2026-10-07 16:53:03

Step 12: Cut the transfer lag with xfr-cycle-interval

The first API change after setup took 48 seconds to reach the secondary. The primary does not notify on every API write; it checks its zones for new serials once per xfr-cycle-interval, 60 seconds by default, and notifies for whatever changed. Five API edits per setting, spaced 9 to 12 seconds apart, were timed from the API call on the primary to the zone committed line in the journal of the secondary (both servers NTP-synchronised):

xfr-cycle-intervalEdit to AXFR commit, five runsWorst case
60 s (default)47.9, 35.7, 23.4, 11.1, 58.9 s~60 s
5 s3.2, 3.9, 4.6, 0.4, 1.1 s~5 s

The delay is set by where the edit falls inside the cycle, not by transfer time, which stayed under 0.4 seconds for this zone. Add the setting on the primary and restart:

echo "xfr-cycle-interval=5" | sudo tee -a /etc/powerdns/pdns.d/pdns.local.conf
sudo systemctl restart pdns

PowerDNS-Admin shows the running value under Server Configuration, which is a quick way to confirm the restart picked it up:

PowerDNS-Admin Server Configuration on Debian filtered to xfr-cycle-interval with the value 5

Five seconds means the primary checks every zone twelve times a minute. The test primary held one zone, so that cost was not measured; with tens of thousands of zones, keep the default and trigger pdns_control notify <zone> from automation instead.

Upgrade an existing Debian 4.9 install to 5.1

The test primary began on the Debian 13 archive package, 4.9.17, configured as in Step 5, with PowerDNS-Admin connected. Take a dump first; the DNSSEC private keys live in the same database:

sudo mysqldump --single-transaction "${PDNS_DB}" | gzip > ~/pdns-before-5.1.sql.gz

Add the repository and pin from Step 4, then install over the existing packages, keeping the current configuration files:

sudo apt update
sudo apt install -y -o Dpkg::Options::=--force-confold pdns-server pdns-backend-mysql

The install took 3.9 seconds and restarted the daemon in place:

Unpacking pdns-backend-mysql (5.1.4-1pdns.debian13) over (4.9.17-0+deb13u1) ...
Unpacking pdns-backend-bind (5.1.4-1pdns.debian13) over (4.9.17-0+deb13u1) ...
Unpacking pdns-server (5.1.4-1pdns.debian13) over (4.9.17-0+deb13u1) ...
Setting up pdns-server (5.1.4-1pdns.debian13) ...
Setting up pdns-backend-mysql (5.1.4-1pdns.debian13) ...
Setting up pdns-backend-bind (5.1.4-1pdns.debian13) ...

The gmysql schema did not change between 4.9 and 5.1, so no SQL upgrade script was needed and the zone answered immediately after the restart. What did break was tooling: the 4.9 command sudo pdnsutil add-record example.internal mail A 3600 10.0.1.82, which this guide used before, now fails because 5.0 and later treat every record name as absolute, even in the old syntax:

Error: Name "mail." to add is not part of zone "example.internal.".

Scripts need the full name, mail.example.internal. Jumping from 4.9 also crosses the 5.0 changes: zone add-key now defaults to a KSK, and record edits on secondary zones need --force. The 5.1 release also stopped sending access-control-allow-origin: * from the API and blocks LUA record updates unless enable-lua-record-updates=yes is set; the official upgrade notes list the remaining changes.

Troubleshooting

Error: “Unable to bind UDP socket to ‘0.0.0.0:53’: Address already in use”

systemd-resolved owns port 53 on the host. Apply Step 2 and run sudo systemctl restart pdns; sudo ss -lntup 'sport = :53' should then show only pdns_server.

Error: Name “mail.” to add is not part of zone

A relative name was passed to pdnsutil on 5.x. Pass the fully qualified name, and the zone name itself for the apex.

The zone answers REFUSED right after creation

Zones created with pdnsutil enter the zone cache only after sudo pdns_control rediscover or the next 300-second refresh. A REFUSED answer that was already served stays in the packet cache for up to 20 seconds more.

The secondary never creates the zone

Check three things in order, starting with the zone kind. The zone kind on the primary must be primary (sudo pdnsutil zone set-kind example.internal primary); a Native zone sends no NOTIFY. The autoprimary entry on the secondary must name the source address of the primary and a host that appears in the NS set of the zone. TCP 53 must be open from the secondary to the primary, since the AXFR itself runs over TCP.

PowerDNS-Admin times out while curl to the API works

UFW is dropping traffic that arrives from the Docker bridge, so add sudo ufw allow in on docker0 to any port 8081 proto tcp. An HTTP 401 instead of a timeout points to a mismatched API key.

Measured throughput and memory

Throughput was measured with dnsperf from the Debian 12 server against the primary over the LAN: 30 seconds, 20 clients, 2 threads, a six-query mix that included one NXDOMAIN name. The no-cache run set cache-ttl=0, query-cache-ttl=0 and negquery-cache-ttl=0 in a temporary drop-in file, which was removed afterwards:

Measurement (primary, 2 vCPU, gmysql)Caches on (default)Caches off
Queries per second141,7085,576
Average latency0.68 ms17.9 ms
Maximum latency3.9 ms78.1 ms
Queries lost0 of 4,251,3110 of 167,428

The load generator was itself a 2 vCPU VM, so the cached figure is a floor set partly by the client, not the server ceiling. The 25-fold gap is the packet and query cache. Real traffic sits between the two columns, closer to the cached figure for a small set of busy names and closer to the uncached one for large zones with a long tail of queries. Every uncached query costs a MariaDB lookup, which is why innodb_buffer_pool_size should hold the records table on a busy primary. Resident memory after the test runs:

ComponentServerResident memory
PowerDNS-Admin containerbench01, Debian 13377 MiB
MariaDBbench01, Debian 13162 MiB
pdns_server, primarybench01, Debian 1366 MiB
pdns_server, secondarybench02, Debian 1268 MiB

The secondary is the cheap tier: one process and an SQLite file, so a 1 GB VM in another location makes a reasonable third name server. For ongoing numbers rather than one-off tests, export the PowerDNS statistics to Prometheus and Grafana, and see BIND vs dnsmasq vs PowerDNS vs Unbound for how PowerDNS compares with the other servers on the same hardware.

Keep reading

Free CCNA 200-301 Practice Questions with Answers Networking Free CCNA 200-301 Practice Questions with Answers Configure Samba File Share on Debian 13 / 12 Debian Configure Samba File Share on Debian 13 / 12 Backup and Restore Linux Systems with Timeshift Debian Backup and Restore Linux Systems with Timeshift Upgrade a Single Package on Ubuntu / Debian with apt Debian Upgrade a Single Package on Ubuntu / Debian with apt APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Cheat Sheets APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Best Point-to-Point Wireless Bridges for a Garage or Outbuilding Networking Best Point-to-Point Wireless Bridges for a Garage or Outbuilding

17 thoughts on “Install PowerDNS and PowerDNS-Admin on Debian 13 / 12”

  1. Excellent article!

    I was able to create a PowerDNS in a Proxmox LXC container by following your step-by-step tutorial.

    The only correction is the command “systemctl reload-daemon”: the correct is “systemctl daemon-reload”

    Thank you so much!

    Reply
  2. What a spectacular guide!!!
    This was my first foray into using PowerDNS and your guide was almost flawless for me.

    One error is you have one of the commands around the wrong way “systemctl reload-daemon” should read “systemctl daemon-reload”.

    This next error was more specific to me as I was setting this up in a domainless environment. Where you create the file “vim /etc/nginx/conf.d/powerdns-admin.conf” and set “server_name pdnsadmin.computingforgeeks.com;” line, this resulted in nginx loading the default homepage (yes I was using my own domain name that I planned to create). I had to drop the domain name, leaving only the hostname.

    Hope that helps anyone else down the line!

    Reply
  3. Thank you for the instructions. Unfortunately I can’t get the web interface to work…

    When I try to access the web interface, I get to the standard NGNIX start page but never get to the interface.

    I am using Debain 11 the rest works except the web interface.

    Can you help me out?

    Reply
  4. Does not work.
    nginx starts up – but shows default page.
    Once changing the default to take your config – the process the new site to login. However gives bad gateway

    Reply
  5. there is an error after “pip install -r requirements.txt”

    Preparing metadata (setup.py) … error
    error: subprocess-exited-with-error

    × python setup.py egg_info did not run successfully.
    │ exit code: 1
    ╰─> [23 lines of output]
    running egg_info
    creating /tmp/pip-pip-egg-info-bs5yvuaq/psycopg2.egg-info
    writing /tmp/pip-pip-egg-info-bs5yvuaq/psycopg2.egg-info/PKG-INFO
    writing dependency_links to /tmp/pip-pip-egg-info-bs5yvuaq/psycopg2.egg-info/dependency_links.txt
    writing top-level names to /tmp/pip-pip-egg-info-bs5yvuaq/psycopg2.egg-info/top_level.txt
    writing manifest file ‘/tmp/pip-pip-egg-info-bs5yvuaq/psycopg2.egg-info/SOURCES.txt’

    Error: pg_config executable not found.

    pg_config is required to build psycopg2 from source. Please add the directory
    containing pg_config to the $PATH or specify the full executable path with the
    option:

    python setup.py build_ext –pg-config /path/to/pg_config build …

    or with the pg_config option in ‘setup.cfg’.

    If you prefer to avoid building psycopg2 from source, please install the PyPI
    ‘psycopg2-binary’ package instead.

    For further information please check the ‘doc/src/install.rst’ file (also at
    ).

    [end of output]

    note: This error originates from a subprocess, and is likely not a problem with pip.
    error: metadata-generation-failed

    × Encountered error while generating package metadata.
    ╰─> See above for output.

    note: This is an issue with the package mentioned above, not pip.
    hint: See above for details.

    Reply
  6. Problem:
    pg_config executable not found.

    Fix:
    apt install libpq-dev

    Problem:
    (flask) root@CT118:/var/www/html/pdns# yarn install –pure-lockfile
    yarn install v1.22.19
    [1/4] Resolving packages…
    warning Resolution field “@fortawesome/[email protected]” is incompatible with requested version “@fortawesome/fontawesome-free@^5.15.4″
    [2/4] Fetching packages…
    [3/4] Linking dependencies…
    warning ” > [email protected]″ has unmet peer dependency “[email protected] – 3″.
    warning ” > [email protected]″ has unmet peer dependency “popper.js@^1.16.1”.
    warning “admin-lte > [email protected]” has incorrect peer dependency “bootstrap@^3.1.1″.
    warning ” > [email protected].5″ has unmet peer dependency “jquery@^1.7 || ^2.0 || ^3.1”.
    warning “admin-lte > [email protected].2″ has unmet peer dependency “moment-timezone@^0.5.31”.
    warning “admin-lte > [email protected].2″ has unmet peer dependency “[email protected]”.
    warning “admin-lte > bootstrap-colorpicker > [email protected]” has unmet peer dependency “@popperjs/core@^2.11.6”.
    [4/4] Building fresh packages…
    [1/3] ⠂ @fortawesome/fontawesome-free
    [2/3] ⠂ es5-ext
    error /var/www/html/pdns/powerdnsadmin/static/node_modules/admin-lte: Command failed.
    Exit code: 127
    Command: npm run plugins
    Arguments:
    Directory: /var/www/html/pdns/powerdnsadmin/static/node_modules/admin-lte
    Output:

    Reply
  7. Hello.

    Thanks for the help, this article really helped me sucessfuly install PowerDNS on my Ubntu server!

    I was just wondering, my dns only works for the zones/host I put it on WEB, and does not resolve any “external” domain (e.g. http://www.google.com).

    Did any of you guys have the same issue and how do I manage to resolve public IPs.

    Best regards!

    Reply

Leave a Comment

Press ESC to close