--- tags: - installation - dns - powerdns created: 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: ```env TZ=Asia/Shanghai PGUSER=postgres PGPASSWORD= DB_HOST=db DB_PORT=5432 DB_NAME=pdns DB_USER=pdns DB_PASS= ADMIN_DB=pdnsadmin CRON_SCHEDULE=0 3 * * * RETENTION_DAYS=7 MAX_BACKUPS=7 DUMP_ROLES=true PDNS_API_KEY= PA_SESSION_KEY= PA_ADMIN_USERNAME=admin PA_ADMIN_PASSWORD= PA_ADMIN_EMAIL=admin@example.com PA_ADMIN_FULLNAME=DNS Administrator PGWEB_USER=pgweb PGWEB_PASS= 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 ```env TZ=Asia/Shanghai PGUSER=postgres PGPASSWORD= DB_HOST=db DB_PORT=5432 DB_NAME=pdns DB_USER=pdns DB_PASS= ADMIN_DB=pdnsadmin CRON_SCHEDULE=0 3 * * * RETENTION_DAYS=7 MAX_BACKUPS=7 DUMP_ROLES=true PDNS_API_KEY= PA_SESSION_KEY= PA_ADMIN_USERNAME=admin PA_ADMIN_PASSWORD= PA_ADMIN_EMAIL=admin@example.com PA_ADMIN_FULLNAME=DNS Administrator PGWEB_USER=pgweb PGWEB_PASS= 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 ```yaml 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 ```ini 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 ```ini 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.