vault backup: 2026-06-24 17:52:15

This commit is contained in:
windyboy
2026-06-24 17:52:15 +08:00
parent 033c9690db
commit 70c4508e56
4 changed files with 2754 additions and 0 deletions
@@ -0,0 +1,679 @@
---
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=<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
```env
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
```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.