20 KiB
tags, created
| tags | 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:
- DNS is exposed on host port
53 - the PowerDNS API is only exposed on
127.0.0.1:8081 - 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:
- Linux
- Docker
docker compose- port
53/tcpand53/udpavailable - a reverse proxy network if Poweradmin and pgweb will be published through Traefik
- 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:
- PostgreSQL admin credentials are defined
- application database credentials are defined
- PowerDNS API key is defined
- Poweradmin admin account values are defined
- pgweb login values are defined
- backup schedule and retention values are defined
3. Start the stack
Start the stack in detached mode.
Expected startup order:
- PostgreSQL starts first
- database bootstrap creates the application role and databases
- PowerDNS starts after PostgreSQL is healthy
- Poweradmin starts after both PostgreSQL and PowerDNS are healthy
- backup service starts cron after PostgreSQL is healthy
- pgweb starts after PostgreSQL is healthy
4. Wait for health checks
The deployment should be considered ready only after:
- PostgreSQL passes
pg_isready - PowerDNS API health check returns an authoritative daemon result
- Poweradmin and pgweb become reachable through the reverse proxy
5. Complete first access
After startup:
- sign in to Poweradmin with the bootstrap admin account
- verify pgweb login works
- verify the PowerDNS API is reachable from the local host only
- 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:
- create the role named by
DB_USERif missing - update the password of
DB_USERfromDB_PASS - create the database named by
DB_NAMEif missing - create the database named by
ADMIN_DBif missing - 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:
- install PostgreSQL client tools and required shell utilities
- set container timezone
- build
/root/.pgpassfor unattended database access - write the cron job using
CRON_SCHEDULE - 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:
- dump the configured application database
- optionally dump PostgreSQL roles
- remove stale temporary files
- remove zero-byte role dumps
- prune backups older than
RETENTION_DAYS - keep only the newest
MAX_BACKUPSbackup 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:
- PostgreSQL is healthy and reachable by dependent services
- PowerDNS answers on TCP and UDP port
53 - the PowerDNS API responds on
127.0.0.1:8081 - the API is not exposed on public interfaces
- Poweradmin login works with the configured admin account
- pgweb login works with the configured credentials
- scheduled backups are being created and rotated
Restore note
Restore should be treated as destructive.
Expected restore flow:
- terminate active connections to the target database
- optionally restore PostgreSQL roles
- drop the target database
- recreate the target database with
DB_USERas owner - import the selected SQL dump
- 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
- Keep live secrets out of static service config where possible.
- Bind the PowerDNS API only to loopback or trusted private ranges.
- Restrict AXFR and notify peers to trusted IP addresses only.
- Publish Poweradmin and pgweb through HTTPS only.
- Protect backup files so only intended operators can read them.