Files
my-vault/01_Projects/Infrastructure/Services/PowerDNS Auth/Powerdns Docker Compose.md
T

20 KiB

tags, created
tags created
installation
dns
powerdns
2026-06-17

PowerDNS Docker Compose

Purpose

This note describes how to install a PowerDNS Authoritative environment with:

  • PostgreSQL 16
  • PowerDNS Authoritative 5.0.4
  • Poweradmin
  • pgweb
  • scheduled PostgreSQL backups

This installation model assumes:

  1. DNS is exposed on host port 53
  2. the PowerDNS API is only exposed on 127.0.0.1:8081
  3. web management services are published through a reverse proxy such as Traefik

Component versions

Component Version
PostgreSQL 16
PowerDNS Authoritative 5.0.4
Poweradmin stable
pgweb 0.16.2
Backup container alpine:3.20

Before you start

Make sure the target host has:

  1. Linux
  2. Docker
  3. docker compose
  4. port 53/tcp and 53/udp available
  5. a reverse proxy network if Poweradmin and pgweb will be published through Traefik
  6. public DNS names for the web interfaces if they will be exposed externally

Deployment values to prepare

Prepare all runtime values before starting the stack.

Required core values

Variable Meaning
PGUSER PostgreSQL administrative user
PGPASSWORD PostgreSQL administrative password
DB_NAME Main PowerDNS database
DB_USER Application database user
DB_PASS Application database password
PDNS_API_KEY PowerDNS API key
CRON_SCHEDULE Backup schedule
PA_SESSION_KEY Poweradmin session secret
PA_ADMIN_USERNAME Bootstrap Poweradmin admin username
PA_ADMIN_PASSWORD Bootstrap Poweradmin admin password
PA_ADMIN_EMAIL Bootstrap Poweradmin admin email
PA_ADMIN_FULLNAME Bootstrap Poweradmin admin full name
PGWEB_USER pgweb login username
PGWEB_PASS pgweb login password

Optional values with defaults

Variable Default Meaning
TZ Asia/Shanghai Service timezone
DB_HOST db Database hostname
DB_PORT 5432 Database port
ADMIN_DB pdnsadmin Admin database used for bootstrap and restore
RETENTION_DAYS 7 Backup age retention
MAX_BACKUPS 7 Number of backup sets to keep
DUMP_ROLES true Include PostgreSQL role dump in backups
PDNS_VERSION 49 Poweradmin PowerDNS compatibility mode
DNS_NS1 ns1.wsvc.info Default NS1 value in Poweradmin
DNS_NS2 ns2.wsvc.info Default NS2 value in Poweradmin
DNS_HOSTMASTER hostmaster.wsvc.info Default SOA hostmaster
PA_APP_TITLE Poweradmin Poweradmin UI title
PA_CREATE_ADMIN 1 Enable bootstrap admin creation

Example values

Replace every placeholder with your own values:

TZ=Asia/Shanghai
PGUSER=postgres
PGPASSWORD=<strong-postgres-password>
DB_HOST=db
DB_PORT=5432
DB_NAME=pdns
DB_USER=pdns
DB_PASS=<strong-app-password>
ADMIN_DB=pdnsadmin
CRON_SCHEDULE=0 3 * * *
RETENTION_DAYS=7
MAX_BACKUPS=7
DUMP_ROLES=true
PDNS_API_KEY=<strong-api-key>
PA_SESSION_KEY=<long-random-session-key>
PA_ADMIN_USERNAME=admin
PA_ADMIN_PASSWORD=<strong-admin-password>
PA_ADMIN_EMAIL=admin@example.com
PA_ADMIN_FULLNAME=DNS Administrator
PGWEB_USER=pgweb
PGWEB_PASS=<strong-pgweb-password>
PDNS_VERSION=49
DNS_NS1=ns1.example.com
DNS_NS2=ns2.example.com
DNS_HOSTMASTER=hostmaster.example.com
PA_APP_TITLE=Poweradmin
PA_CREATE_ADMIN=1

Full configuration blocks

Use the following full configuration content as the installation baseline.

Environment file

TZ=Asia/Shanghai
PGUSER=postgres
PGPASSWORD=<strong-postgres-password>
DB_HOST=db
DB_PORT=5432
DB_NAME=pdns
DB_USER=pdns
DB_PASS=<strong-app-password>
ADMIN_DB=pdnsadmin
CRON_SCHEDULE=0 3 * * *
RETENTION_DAYS=7
MAX_BACKUPS=7
DUMP_ROLES=true
PDNS_API_KEY=<strong-api-key>
PA_SESSION_KEY=<long-random-session-key>
PA_ADMIN_USERNAME=admin
PA_ADMIN_PASSWORD=<strong-admin-password>
PA_ADMIN_EMAIL=admin@example.com
PA_ADMIN_FULLNAME=DNS Administrator
PGWEB_USER=pgweb
PGWEB_PASS=<strong-pgweb-password>
PDNS_VERSION=49
DNS_NS1=ns1.example.com
DNS_NS2=ns2.example.com
DNS_HOSTMASTER=hostmaster.example.com
PA_APP_TITLE=Poweradmin
PA_CREATE_ADMIN=1

Docker Compose configuration

networks:
  frontend:
    name: traefik
    external: true

  backend:
    internal: true

  edge:

services:
  db:
    image: postgres:16
    container_name: pdns-db
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: ${PGUSER:?missing PGUSER}
      POSTGRES_PASSWORD: ${PGPASSWORD:?missing PGPASSWORD}
      TZ: ${TZ:-Asia/Shanghai}
      PGTZ: ${TZ:-Asia/Shanghai}
    volumes:
      - dbdata:/var/lib/postgresql
      - ./db-init-generated:/docker-entrypoint-initdb.d:ro
      - ./backup:/backup:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
      interval: 10s
      timeout: 5s
      retries: 10
    restart: unless-stopped
    networks: [backend, edge]

  auth:
    image: powerdns/pdns-auth-50:5.0.4
    container_name: pdns-auth
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "53:53/udp"
      - "53:53/tcp"
      - "127.0.0.1:8081:8081"
    environment:
      PDNS_API_KEY: ${PDNS_API_KEY:?missing PDNS_API_KEY}
      DB_NAME: ${DB_NAME:?missing DB_NAME}
      DB_USER: ${DB_USER:?missing DB_USER}
      DB_PASS: ${DB_PASS:?missing DB_PASS}
      TEMPLATE_FILES: secrets
    volumes:
      - ./auth/pdns.conf:/etc/powerdns/pdns.conf:ro
      - ./auth/templates.d:/etc/powerdns/templates.d:ro
      - ./auth/keys:/var/lib/powerdns
      - ./auth/import:/import
      - ./auth/export:/export
      - ./auth/logs:/var/log/pdns
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "python3 -c \"import json, os, urllib.request; req = urllib.request.Request('http://127.0.0.1:8081/api/v1/servers/localhost', headers={'X-API-Key': os.environ['PDNS_API_KEY']}); data = json.load(urllib.request.urlopen(req, timeout=3)); assert data['daemon_type'] == 'authoritative'\""
        ]
      interval: 10s
      timeout: 5s
      retries: 12
    restart: unless-stopped
    networks: [backend, edge]

  poweradmin:
    image: poweradmin/poweradmin:stable
    container_name: poweradmin
    depends_on:
      db:
        condition: service_healthy
      auth:
        condition: service_healthy
    environment:
      DB_TYPE: pgsql
      DB_HOST: ${DB_HOST:-db}
      DB_PORT: ${DB_PORT:-5432}
      DB_NAME: ${DB_NAME:?missing DB_NAME}
      DB_USER: ${DB_USER:?missing DB_USER}
      DB_PASS: ${DB_PASS:?missing DB_PASS}
      PA_PDNS_API_URL: http://auth:8081
      PA_PDNS_API_KEY: ${PDNS_API_KEY:?missing PDNS_API_KEY}
      PA_DNS_BACKEND: sql
      PDNS_VERSION: ${PDNS_VERSION:-49}
      DNS_NS1: ${DNS_NS1:-ns1.wsvc.info}
      DNS_NS2: ${DNS_NS2:-ns2.wsvc.info}
      DNS_HOSTMASTER: ${DNS_HOSTMASTER:-hostmaster.wsvc.info}
      PA_APP_TITLE: ${PA_APP_TITLE:-Poweradmin}
      PA_TIMEZONE: ${TZ:-Asia/Shanghai}
      PA_SESSION_KEY: ${PA_SESSION_KEY:?missing PA_SESSION_KEY}
      PA_CREATE_ADMIN: ${PA_CREATE_ADMIN:-1}
      PA_ADMIN_USERNAME: ${PA_ADMIN_USERNAME:?missing PA_ADMIN_USERNAME}
      PA_ADMIN_PASSWORD: ${PA_ADMIN_PASSWORD:?missing PA_ADMIN_PASSWORD}
      PA_ADMIN_EMAIL: ${PA_ADMIN_EMAIL:?missing PA_ADMIN_EMAIL}
      PA_ADMIN_FULLNAME: ${PA_ADMIN_FULLNAME:?missing PA_ADMIN_FULLNAME}
      TRUSTED_PROXIES: private_ranges
      DEBUG: "false"
    restart: unless-stopped
    networks: [backend, frontend]
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.poweradmin.rule=Host(`pdns.wsvc.info`)"
      - "traefik.http.routers.poweradmin.entrypoints=websecure"
      - "traefik.http.routers.poweradmin.tls.certresolver=letsencrypt"
      - "traefik.http.services.poweradmin.loadbalancer.server.port=80"

  backup:
    image: alpine:3.20
    container_name: pdns-backup
    depends_on:
      db:
        condition: service_healthy
    environment:
      TZ: ${TZ:-Asia/Shanghai}
      DB_HOST: ${DB_HOST:-db}
      DB_PORT: ${DB_PORT:-5432}
      DB_USER: ${PGUSER:?missing PGUSER}
      DB_PASS: ${PGPASSWORD:?missing PGPASSWORD}
      DB_NAME: ${DB_NAME:?missing DB_NAME}
      RETENTION_DAYS: ${RETENTION_DAYS:-7}
      MAX_BACKUPS: ${MAX_BACKUPS:-7}
      DUMP_ROLES: ${DUMP_ROLES:-true}
      CRON_SCHEDULE: ${CRON_SCHEDULE:?missing CRON_SCHEDULE}
    volumes:
      - ./backup:/backup
      - ./scripts:/scripts:ro
    entrypoint: >
      sh -c '
        apk add --no-cache postgresql16-client tzdata util-linux bash coreutils findutils;
        ln -snf /usr/share/zoneinfo/$$TZ /etc/localtime && echo $$TZ > /etc/timezone;
        echo "$$DB_HOST:$$DB_PORT:*:$$DB_USER:$$DB_PASS" > /root/.pgpass;
        chmod 600 /root/.pgpass;
        echo "$$CRON_SCHEDULE flock -n /backup/.backup.lock /scripts/backup.sh >> /backup/backup.log 2>&1" > /etc/crontabs/root;
        echo "[$$(date -Iseconds)] cron started with schedule: $$CRON_SCHEDULE" >> /backup/backup.log;
        crond -f -l 8
      '
    restart: unless-stopped
    networks: [backend]

  pgweb:
    image: sosedoff/pgweb:0.16.2
    container_name: pdns_pgweb
    restart: unless-stopped
    environment:
      PGWEB_DATABASE_URL: "postgres://${PGUSER:?missing PGUSER}:${PGPASSWORD:?missing PGPASSWORD}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:?missing DB_NAME}?sslmode=disable"
      PGWEB_AUTH_USER: ${PGWEB_USER:?missing PGWEB_USER}
      PGWEB_AUTH_PASS: ${PGWEB_PASS:?missing PGWEB_PASS}
      TZ: ${TZ:-Asia/Shanghai}
    depends_on:
      db:
        condition: service_healthy
    networks: [backend, frontend]
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.pgweb.rule=Host(`pgweb.wsvc.info`)"
      - "traefik.http.routers.pgweb.entrypoints=websecure"
      - "traefik.http.routers.pgweb.tls.certresolver=letsencrypt"
      - "traefik.http.services.pgweb.loadbalancer.server.port=8081"

volumes:
  dbdata: {}

PowerDNS daemon configuration

local-address=0.0.0.0
local-port=53

launch=gpgsql
gpgsql-host=db
include-dir=/etc/powerdns/pdns.d
gpgsql-dnssec=yes
allow-axfr-ips=202.91.35.141
also-notify=202.91.35.141
primary=yes
secondary=no

api=yes
webserver=yes
webserver-address=0.0.0.0
webserver-port=8081
webserver-allow-from=127.0.0.1,172.16.0.0/12,10.0.0.0/8,192.168.0.0/16

version-string=anonymous
disable-syslog=yes
loglevel=4

default-soa-edit=INCEPTION-INCREMENT
default-soa-edit-signed=INCEPTION-INCREMENT

disable-axfr=no

Runtime-rendered secret template

gpgsql-dbname={{ DB_NAME }}
gpgsql-user={{ DB_USER }}
gpgsql-password={{ DB_PASS }}
api-key={{ PDNS_API_KEY }}

Network layout

The stack uses three networks:

Network Purpose
backend internal service-to-service traffic
edge local host-facing DNS and API exposure
frontend reverse-proxy-facing web traffic

Service attachment:

Service Networks
PostgreSQL backend, edge
PowerDNS auth backend, edge
Poweradmin backend, frontend
Backup backend
pgweb backend, frontend

Installation procedure

1. Prepare the host

Install Docker and Docker Compose support on the Linux host.

If a reverse proxy network is required, create it before deployment.

2. Prepare runtime configuration

Set all required environment values.

At minimum, verify:

  1. PostgreSQL admin credentials are defined
  2. application database credentials are defined
  3. PowerDNS API key is defined
  4. Poweradmin admin account values are defined
  5. pgweb login values are defined
  6. backup schedule and retention values are defined

3. Start the stack

Start the stack in detached mode.

Expected startup order:

  1. PostgreSQL starts first
  2. database bootstrap creates the application role and databases
  3. PowerDNS starts after PostgreSQL is healthy
  4. Poweradmin starts after both PostgreSQL and PowerDNS are healthy
  5. backup service starts cron after PostgreSQL is healthy
  6. pgweb starts after PostgreSQL is healthy

4. Wait for health checks

The deployment should be considered ready only after:

  1. PostgreSQL passes pg_isready
  2. PowerDNS API health check returns an authoritative daemon result
  3. Poweradmin and pgweb become reachable through the reverse proxy

5. Complete first access

After startup:

  1. sign in to Poweradmin with the bootstrap admin account
  2. verify pgweb login works
  3. verify the PowerDNS API is reachable from the local host only
  4. verify DNS answers on port 53

Service configuration details

PostgreSQL

Setting Value
Image postgres:16
Container name pdns-db
Restart policy unless-stopped
Startup database postgres
Health check pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
Timezone TZ, PGTZ

Bootstrap behavior:

  1. create the role named by DB_USER if missing
  2. update the password of DB_USER from DB_PASS
  3. create the database named by DB_NAME if missing
  4. create the database named by ADMIN_DB if missing
  5. assign ownership of both databases to DB_USER

PowerDNS Authoritative

Setting Value
Image powerdns/pdns-auth-50:5.0.4
Container name pdns-auth
Restart policy unless-stopped
Published ports 53/tcp, 53/udp, 127.0.0.1:8081
Health check local API request using PDNS_API_KEY

Environment used by PowerDNS:

Variable Meaning
PDNS_API_KEY API authentication key
DB_NAME PowerDNS PostgreSQL database
DB_USER PowerDNS PostgreSQL user
DB_PASS PowerDNS PostgreSQL password
TEMPLATE_FILES=secrets enables runtime rendering of dynamic config

Rendered runtime directives:

Directive Source
gpgsql-dbname DB_NAME
gpgsql-user DB_USER
gpgsql-password DB_PASS
api-key PDNS_API_KEY

Daemon behavior:

Directive Value Meaning
local-address 0.0.0.0 listen on all container interfaces
local-port 53 DNS listener port
launch gpgsql PostgreSQL backend
gpgsql-host db database service hostname
gpgsql-dnssec yes DNSSEC enabled
api yes API enabled
webserver yes embedded web server enabled
webserver-address 0.0.0.0 listen on all container interfaces
webserver-port 8081 API port
webserver-allow-from 127.0.0.1,172.16.0.0/12,10.0.0.0/8,192.168.0.0/16 restrict API access to local and private ranges
primary yes primary DNS role enabled
secondary no secondary role disabled
allow-axfr-ips 202.91.35.141 allowed AXFR peer
also-notify 202.91.35.141 notify destination
version-string anonymous hide version string
disable-syslog yes disable syslog
loglevel 4 logging verbosity
default-soa-edit INCEPTION-INCREMENT SOA serial update policy
default-soa-edit-signed INCEPTION-INCREMENT SOA serial policy for signed zones
disable-axfr no AXFR globally allowed if otherwise permitted

Poweradmin

Setting Value
Image poweradmin/poweradmin:stable
Container name poweradmin
Restart policy unless-stopped
Database type pgsql
API endpoint http://auth:8081
DNS backend mode sql
Trusted proxies private_ranges
Debug false

Important Poweradmin values:

Variable Value
DB_HOST db by default
DB_PORT 5432 by default
DB_NAME required
DB_USER required
DB_PASS required
PA_PDNS_API_KEY same value as PDNS_API_KEY
PDNS_VERSION 49
DNS_NS1 configurable default nameserver
DNS_NS2 configurable default nameserver
DNS_HOSTMASTER configurable default hostmaster
PA_APP_TITLE Poweradmin by default
PA_TIMEZONE value from TZ
PA_SESSION_KEY required
PA_CREATE_ADMIN 1 by default
PA_ADMIN_USERNAME required
PA_ADMIN_PASSWORD required
PA_ADMIN_EMAIL required
PA_ADMIN_FULLNAME required

Reverse proxy routing:

Item Value
Router host pdns.wsvc.info
Entry point websecure
TLS resolver letsencrypt
Internal service port 80

Backup service

Setting Value
Image alpine:3.20
Container name pdns-backup
Restart policy unless-stopped
Trigger mode cron inside the container
Locking flock -n

Startup behavior:

  1. install PostgreSQL client tools and required shell utilities
  2. set container timezone
  3. build /root/.pgpass for unattended database access
  4. write the cron job using CRON_SCHEDULE
  5. start cron in foreground mode

Backup variables:

Variable Value
DB_HOST db by default
DB_PORT 5432 by default
DB_USER same as PGUSER
DB_PASS same as PGPASSWORD
DB_NAME required
RETENTION_DAYS 7 by default
MAX_BACKUPS 7 by default
DUMP_ROLES true by default
CRON_SCHEDULE required

Backup behavior:

  1. dump the configured application database
  2. optionally dump PostgreSQL roles
  3. remove stale temporary files
  4. remove zero-byte role dumps
  5. prune backups older than RETENTION_DAYS
  6. keep only the newest MAX_BACKUPS backup sets

Backup artifact patterns:

Artifact Pattern
Database dump pdns_YYYY-MM-DD_HH-MM-SS.sql.gz
Roles dump roles_YYYY-MM-DD_HH-MM-SS.sql

pgweb

Setting Value
Image sosedoff/pgweb:0.16.2
Container name pdns_pgweb
Restart policy unless-stopped
Database URL mode PostgreSQL DSN with sslmode=disable
Login user PGWEB_USER
Login password PGWEB_PASS

Reverse proxy routing:

Item Value
Router host pgweb.wsvc.info
Entry point websecure
TLS resolver letsencrypt
Internal service port 8081

Port exposure summary

Host binding Container port Purpose
53/udp 53/udp DNS over UDP
53/tcp 53/tcp DNS over TCP
127.0.0.1:8081 8081 local PowerDNS API

Post-install checks

After installation, verify all of the following:

  1. PostgreSQL is healthy and reachable by dependent services
  2. PowerDNS answers on TCP and UDP port 53
  3. the PowerDNS API responds on 127.0.0.1:8081
  4. the API is not exposed on public interfaces
  5. Poweradmin login works with the configured admin account
  6. pgweb login works with the configured credentials
  7. scheduled backups are being created and rotated

Restore note

Restore should be treated as destructive.

Expected restore flow:

  1. terminate active connections to the target database
  2. optionally restore PostgreSQL roles
  3. drop the target database
  4. recreate the target database with DB_USER as owner
  5. import the selected SQL dump
  6. reassign schema ownership if needed

Restore control values:

Variable Meaning
DB_NAME target database to recreate
DB_USER owner of the restored database
ADMIN_DB administrative database used during restore
ADMIN_USER administrative PostgreSQL user, default postgres
RESTORE_ROLES auto, always, or never
CONFIRM_RESTORE=YES skip confirmation prompt
ROLES_FILE explicit role dump file

Security notes

  1. Keep live secrets out of static service config where possible.
  2. Bind the PowerDNS API only to loopback or trusted private ranges.
  3. Restrict AXFR and notify peers to trusted IP addresses only.
  4. Publish Poweradmin and pgweb through HTTPS only.
  5. Protect backup files so only intended operators can read them.