### Powerdns ns1 hk2.chans.xyz with docker compose: 好的,按你要的思路来:**仍然用“官方 schema 自动建表”的 Compose 方案**(PostgreSQL 16 + PowerDNS Authoritative),**另外提供一个“手动恢复数据”的脚本**,支持恢复纯 SQL dump 和 `pg_dump -Fc` 自定义格式 dump。 --- # 目录结构(建议) ``` pdns/ ├─ docker-compose.yml ├─ auth/ │ └─ pdns.conf ├─ db-init/ │ └─ 01-pdns-schema.sql # 官方 gpgsql schema(见下文获取方式) ├─ backup/ │ └─ pdns.dump # 你的备份文件(.sql 或 -Fc) └─ scripts/ └─ restore-pdns.sh # 手动恢复脚本 ``` ## 1) 获取 PowerDNS 官方 PostgreSQL schema > **重要**:请下载与你将使用的 `powerdns/pdns-auth-49` **相同小版本**的 schema(例如 4.9.x 对应的 tag),保存为 `db-init/01-pdns-schema.sql`。 > (官方路径通常在 `modules/gpgsqlbackend/schema.pgsql.sql` 对应版本的 tag 下。) --- ## 2) `docker-compose.yml`(保持“官方 schema 建表”的方案) ```yaml version: "3.9" services: db: image: postgres:16 container_name: pdns-db environment: POSTGRES_DB: powerdns POSTGRES_USER: powerdns POSTGRES_PASSWORD: ${PDNS_DB_PASSWORD:-changeme_db} volumes: - dbdata:/var/lib/postgresql/data - ./db-init:/docker-entrypoint-initdb.d:ro # 首启时自动导入 schema - ./backup:/backup:ro # 供手动恢复脚本读取 dump healthcheck: test: ["CMD-SHELL", "pg_isready -U powerdns -d powerdns"] interval: 10s timeout: 5s retries: 10 restart: unless-stopped networks: [dnsnet] auth: # 固定到你要用的 4.9.x 小版本,例如 4.9.4 image: powerdns/pdns-auth-49:4.9.4 container_name: pdns-auth depends_on: db: condition: service_healthy ports: - "53:53/udp" - "53:53/tcp" - "8081:8081" # REST API / 内置 webserver volumes: - ./auth/pdns.conf:/etc/powerdns/pdns.conf:ro environment: PDNS_API_KEY: ${PDNS_API_KEY:-changeme_api} healthcheck: test: ["CMD", "sh", "-c", "wget -qO- --header='X-API-Key: ${PDNS_API_KEY:-changeme_api}' http://127.0.0.1:8081/api/v1/servers/localhost | grep -q 'version'"] interval: 10s timeout: 5s retries: 12 restart: unless-stopped networks: [dnsnet] networks: dnsnet: driver: bridge volumes: dbdata: {} ``` ## 3) `auth/pdns.conf`(PostgreSQL 后端示例) ```ini local-address=0.0.0.0 local-port=53 launch=gpgsql gpgsql-host=pdns-db gpgsql-port=5432 gpgsql-dbname=powerdns gpgsql-user=powerdns gpgsql-password=changeme_db api=yes api-key=changeme_api webserver=yes webserver-address=0.0.0.0 webserver-port=8081 webserver-allow-from=127.0.0.1,10.0.0.0/8,192.168.0.0/16 version-string=anonymous disable-syslog=yes loglevel=4 ``` ## 4) 启动 ```bash docker compose up -d # 等 db 健康检查 OK 后,auth 会启动并可通过 8081 API 访问 ``` --- # 手动恢复脚本(支持 .sql 与 -Fc) `scripts/restore-pdns.sh`:默认**安全模式**是恢复到一个**新数据库**(避免与你现有 schema 冲突),完成后你只需把 `pdns.conf` 的 `gpgsql-dbname` 改成新库名并 `docker compose restart auth` 即可。也提供 `--inplace` 选项可“原地覆盖”(会清空原库 `public` 模式)——谨慎使用。 ```bash #!/usr/bin/env bash set -euo pipefail # 用法: # scripts/restore-pdns.sh /absolute/or/relative/path/to/backup/pdns.dump # 可选: --inplace # 原地覆盖到 powerdns 库(会清空 public schema) # # 说明: # - 支持两类 dump: # 1) 纯 SQL # 2) pg_dump -Fc 自定义格式 # - 默认行为: 恢复到新库 powerdns_restore_YYYYmmddHHMMSS # - 容器/数据库参数需与 docker-compose.yml 一致 DB_SVC="db" # Compose 中的服务名 DB_NAME="powerdns" DB_USER="powerdns" INPLACE=0 if [[ "${1:-}" == "--inplace" ]]; then INPLACE=1 shift fi if [[ $# -lt 1 ]]; then echo "Usage: $0 [--inplace] " exit 1 fi DUMP_PATH="$1" if [[ ! -f "$DUMP_PATH" ]]; then echo "Dump file not found: $DUMP_PATH" exit 1 fi # 统一让容器内能看到该文件(compose 已挂载 ./backup -> /backup:ro) # 若传入的不是 ./backup 下的文件,临时 cp 进去容器使用 IN_CONTAINER_DUMP="" if [[ "$DUMP_PATH" == ./backup/* || "$DUMP_PATH" == backup/* ]]; then # 剥离前缀,映射到 /backup BN="${DUMP_PATH##*/}" IN_CONTAINER_DUMP="/backup/${BN}" else # 复制到容器临时路径 BN="$(basename "$DUMP_PATH")" echo "Copying dump into container..." docker compose cp "$DUMP_PATH" "${DB_SVC}:/tmp/${BN}" IN_CONTAINER_DUMP="/tmp/${BN}" fi # 检测 dump 类型(在容器内使用 'file') FILE_OUT="$(docker compose exec -T ${DB_SVC} sh -lc "file -b ${IN_CONTAINER_DUMP} || true")" echo "Detected: ${FILE_OUT}" IS_FC=0 if echo "$FILE_OUT" | grep -qi 'PostgreSQL custom database dump'; then IS_FC=1 fi if [[ $INPLACE -eq 1 ]]; then echo ">>> INPLACE mode: dropping and recreating schema 'public' in ${DB_NAME}" docker compose exec -T ${DB_SVC} psql -U "${DB_USER}" -d "${DB_NAME}" -v ON_ERROR_STOP=1 <<'SQL' DO $$ BEGIN EXECUTE 'DROP SCHEMA IF EXISTS public CASCADE'; EXECUTE 'CREATE SCHEMA public AUTHORIZATION powerdns'; EXECUTE 'GRANT ALL ON SCHEMA public TO powerdns'; END$$; SQL if [[ $IS_FC -eq 1 ]]; then echo ">>> Restoring (-Fc) into ${DB_NAME} ..." docker compose exec -T ${DB_SVC} sh -lc \ "pg_restore --clean --if-exists --no-owner -U ${DB_USER} -d ${DB_NAME} ${IN_CONTAINER_DUMP}" else echo ">>> Restoring (SQL) into ${DB_NAME} ..." docker compose exec -T ${DB_SVC} sh -lc \ "psql -v ON_ERROR_STOP=1 -U ${DB_USER} -d ${DB_NAME} -f ${IN_CONTAINER_DUMP}" fi echo ">>> Done. INPLACE restore finished for DB: ${DB_NAME}" else NEWDB="powerdns_restore_$(date +%Y%m%d%H%M%S)" echo ">>> Creating new database: ${NEWDB}" docker compose exec -T ${DB_SVC} sh -lc \ "psql -U ${DB_USER} -d postgres -v ON_ERROR_STOP=1 -c 'CREATE DATABASE ${NEWDB} OWNER ${DB_USER};'" if [[ $IS_FC -eq 1 ]]; then echo ">>> Restoring (-Fc) into ${NEWDB} ..." docker compose exec -T ${DB_SVC} sh -lc \ "pg_restore --clean --if-exists --no-owner -U ${DB_USER} -d ${NEWDB} ${IN_CONTAINER_DUMP}" else echo ">>> Restoring (SQL) into ${NEWDB} ..." docker compose exec -T ${DB_SVC} sh -lc \ "psql -v ON_ERROR_STOP=1 -U ${DB_USER} -d ${NEWDB} -f ${IN_CONTAINER_DUMP}" fi echo ">>> Done. Now edit ./auth/pdns.conf:" echo " gpgsql-dbname=${NEWDB}" echo ">>> Then restart PDNS:" echo " docker compose restart auth" fi ``` 给执行权限: ```bash chmod +x scripts/restore-pdns.sh ``` ### 使用示例 1. **默认安全模式(恢复到新库,不影响当前运行):** ```bash ./scripts/restore-pdns.sh ./backup/pdns.dump # 按提示把 auth/pdns.conf 的 gpgsql-dbname 改为脚本输出的新库名 docker compose restart auth ``` 2. **原地覆盖(会清空当前库的 public schema):** ```bash ./scripts/restore-pdns.sh --inplace ./backup/pdns.dump ``` --- ## 小结 / 注意点 - **官方 schema 自动建表**:通过 `db-init/01-pdns-schema.sql` 在首次初始化时创建所需表结构,之后你随时可以用上面的脚本**手动恢复**。 - **避免冲突**:若你的 dump 是“包含 schema 的完整备份”,默认脚本会恢复到**新库**,避免与已存在的官方 schema 冲突;你也可以用 `--inplace` 覆盖恢复。 - **版本匹配**:尽量让 dump 的来源 PostgreSQL 版本 ≤ 目标版本(例如从 13/14/15 恢复到 16 一般可行);PowerDNS 版本与 schema 文件要匹配同一小版本。 - **安全回滚**:新库恢复方式便于回滚——只需把 `pdns.conf` 切回旧库名并 `restart auth`。 需要我把「下载 schema 的命令」按你的目标 `pdns-auth` 版本具体写死(例如 4.9.4)吗?我可以直接给出一条 `curl` 命令放到 README 里。 ## 4.9.8: #### db init ``` curl -L \ https://raw.githubusercontent.com/PowerDNS/pdns/master/modules/gpgsqlbackend/schema.pgsql.sql \ -o db-init/01-pdns-schema.sql ``` ``` docker compose exec -T db \ pg_restore --clean --if-exists --no-owner \ -U pdns -d pdns /backup/pdns.dump ``` ``` docker compose exec -e PGPASSWORD=windyboy2006 -T db \ pg_restore --jobs=4 --clean --if-exists --no-owner --no-acl \ -U pdns -d pdns /backup/pdns.dump ``` ``` docker compose exec -T db psql -U pdns -d pdns -c "SELECT count(*) FROM domains;" ``` db-init/02-pda.sql ```sql -- 创建 PowerDNS-Admin 的数据库与用户(与 PDNS 库隔离) CREATE USER pdnsadmin WITH PASSWORD 'windyboy2006'; CREATE DATABASE pdnsadmin OWNER pdnsadmin ENCODING 'UTF8'; GRANT ALL PRIVILEGES ON DATABASE pdnsadmin TO pdnsadmin; ``` ```shell docker compose exec -T db psql -U ${PDNS_DB_USER:-pdns} -d postgres -v ON_ERROR_STOP=1 \ -c "DO \$\$BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='pdnsadmin') THEN CREATE ROLE pdnsadmin LOGIN PASSWORD 'changeme_pdapass'; END IF; END\$\$;" docker compose exec -T db psql -U ${PDNS_DB_USER:-pdns} -d postgres -v ON_ERROR_STOP=1 \ -c "CREATE DATABASE pdnsadmin OWNER pdnsadmin;" || true ``` ```shell docker compose exec -T db psql \ -U "${PDNS_DB_USER:-powerdns}" -d postgres -v ON_ERROR_STOP=1 \ -c "ALTER ROLE \"${PDNSADMIN_DB_USER:-pdns}\" WITH PASSWORD '${PDNSADMIN_DB_PASSWORD}';" ``` ``` pda: image: powerdnsadmin/pda-legacy:latest container_name: powerdns-admin depends_on: db: condition: service_healthy auth: condition: service_started ports: - "${PDA_HTTP_PORT:-9191}:80" environment: SECRET_KEY: ${PDA_SECRET_KEY:-changeme_pda_secret} SQLALCHEMY_DATABASE_URI: >- postgresql://${PDNSADMIN_DB_USER:-pdnsadmin}:${PDNSADMIN_DB_PASSWORD:-changeme_pdapass}@db:5432/${PDNSADMIN_DB:-pdnsadmin} restart: unless-stopped networks: [dnsnet] ``` ``` docker compose exec -T db psql -U pdns -d postgres -v ON_ERROR_STOP=1 -c "ALTER ROLE pdnsadmin WITH PASSWORD 'windyboy2006';" ``` ``` docker compose exec -e PGPASSWORD="$PDNSADMIN_DB_PASSWORD" -T db \ psql -U "${PDNSADMIN_DB_USER:-pdnsadmin}" -d "${PDNSADMIN_DB:-pdnsadmin}" \ -c "select current_user, current_database();" ``` ```shell docker compose exec auth pdnsutil list-zone chans.xyz ``` ``` docker compose exec auth pdnsutil add-record chans.xyz hk2 A 154.36.174.161 ``` ``` docker compose exec auth pdnsutil delete-rrset example.test www AAAA ``` # PowerDNS Auth 变更 IP — 简版操作手册(按本次实际操作) > 场景回顾:主节点(Docker,**154.36.174.161**)更换/确认公网 IP;二级节点 **us1.wsvc.info** 为二进制安装(非 Docker)。前端用 **Nginx Proxy Manager (NPM)** 代理 **PowerDNS-Admin (PDA)** 与 **PDNS API**。 --- ## 1) 现状与目标 - **主节点(Docker)**:`pdns-auth-49:4.9.8` + `postgres:16` + `powerdnsadmin/pda-legacy`。 - **从节点(us1)**:二进制 `pdns-server`,数据库 `domains` 使用老式字段 `type/master`。 - **NPM 单独 compose**,与 PDNS/PDA 通过 **共享外部网络 `npm_proxy`** 互通。 - 对外 **只开放 53/tcp, 53/udp**;PDA 与 PDNS API 仅在容器网络内,由 NPM 反代并做 IP 白名单。 --- ## 2) 主节点(Docker)操作 ### 2.1 网络与 Compose ```bash docker network create npm_proxy || true ``` **pdns compose 关键点:** - `auth`: - `ports`: 只保留 `53:53/udp`、`53:53/tcp`。 - `expose`: `8081`(API 仅在容器网络可见)。 - `networks`: 加入 `default` + `npm_proxy`。 - **挂载目录**:`./auth:/etc/powerdns:ro`(避免 pdns.conf 丢失)。 - 健康检查可用 `curl`:`curl -fsS -H 'X-API-Key: ${PDNS_API_KEY}' http://127.0.0.1:${PDNS_API_PORT}/api/v1/servers/localhost`。 - `pda`: - **移除** `9191:80` 的对外发布;仅 `expose: 80`,加入 `npm_proxy` 网络。 ### 2.2 `auth/pdns.conf` 关键参数 ```ini api=yes api-key=<你的长随机值> webserver=yes webserver-address=0.0.0.0 webserver-port=8081 webserver-allow-from=127.0.0.1,::1,172.16.0.0/12 launch=gpgsql # gpgsql-* 与 Postgres 账户一致 ``` > **提示**:若本机 `curl http://localhost:8081` 空回应,多半命中 IPv6 `::1`;用 `curl -4` 或把 `::1` 加入 `webserver-allow-from`。 ### 2.3 通过 NPM 反代 - NPM `app` 服务加入 `npm_proxy`;发布 `80/443`。 - 新建 Proxy Host: - **PDA** → `pda:80`(绑定域名,Access List 白名单)。 - **PDNS API** → `auth:8081`(同上)。 > 在 NPM 容器内自检: ```bash docker compose exec app curl -sI http://pda:80/ | head docker compose exec app curl -sI http://auth:8081/api/v1/servers/localhost | head ``` ### 2.4 记录修改(本次实际) - **ns1.wsvc.info A** 改为 `154.36.174.161`: ```bash curl -s -X PATCH -H "X-API-Key: $PDNS_API_KEY" -H 'Content-Type: application/json' \ http://127.0.0.1:8081/api/v1/servers/localhost/zones/wsvc.info. \ -d '{"rrsets":[{"name":"ns1.wsvc.info.","type":"A","changetype":"REPLACE","ttl":86400,"records":[{"content":"154.36.174.161","disabled":false}]}]}' ``` - **hk2.chans.xyz A** 移除 `.101`,保留 `.161`: ```bash curl -s -X PATCH -H "X-API-Key: $PDNS_API_KEY" -H 'Content-Type: application/json' \ http://127.0.0.1:8081/api/v1/servers/localhost/zones/chans.xyz. \ -d '{"rrsets":[{"name":"hk2.chans.xyz.","type":"A","changetype":"REPLACE","ttl":3600,"records":[{"content":"154.36.174.161","disabled":false}]}]}' ``` - 验证:`dig @127.0.0.1 ns1.wsvc.info A +short`、`dig @127.0.0.1 hk2.chans.xyz A +short`。 --- ## 3) us1(二进制从节点)操作(本次实际) > us1 使用二进制 `pdns-server`,数据库 `domains` 为旧 schema(`type/master`)。本次已验证以下两种方式皆可;**推荐优先使用 pdnsutil 命令**。 ### 3.1 推荐:用 `pdnsutil` 指向新主并拉取 确保允许从区: ``` # /etc/powerdns/pdns.conf secondary=yes # 旧版本为 slave=yes ``` 把各区改为 Secondary,并设置新的主(**154.36.174.161**),然后触发 AXFR: ```bash sudo pdnsutil set-kind wsvc.info secondary sudo pdnsutil set-kind chans.xyz secondary sudo pdnsutil set-kind windy.me secondary # 你的 pdnsutil 支持:change-secondary-zone-primary sudo pdnsutil change-secondary-zone-primary wsvc.info 154.36.174.161 sudo pdnsutil change-secondary-zone-primary chans.xyz 154.36.174.161 sudo pdnsutil change-secondary-zone-primary windy.me 154.36.174.161 # 立即拉取(若无此命令可重启 pdns 替代) sudo pdnsutil retrieve-secondary wsvc.info sudo pdnsutil retrieve-secondary chans.xyz sudo pdnsutil retrieve-secondary windy.me ``` 日志与连通性: ```bash journalctl -u pdns -e | egrep -i 'SOA|AXFR|IXFR|NOTIFY' dig @154.36.174.161 wsvc.info SOA +tcp +time=2 +tries=1 ``` > 失败多为 **TCP/53 未通** 或主节点未监听 TCP/53。 ### 3.2 备选 A:没有该子命令时,用“重建从区” ```bash sudo pdnsutil delete-zone wsvc.info && sudo pdnsutil create-secondary-zone wsvc.info 154.36.174.161 sudo pdnsutil delete-zone chans.xyz && sudo pdnsutil create-secondary-zone chans.xyz 154.36.174.161 sudo pdnsutil delete-zone windy.me && sudo pdnsutil create-secondary-zone windy.me 154.36.174.161 ``` ### 3.3 备选 B:直接改数据库后补拉取(你本次已用) ```sql -- 在 us1 上 UPDATE domains SET type='SLAVE', master='154.36.174.161' WHERE name IN ('wsvc.info','chans.xyz','windy.me'); ``` 然后: ```bash sudo pdnsutil retrieve-secondary wsvc.info || sudo systemctl restart pdns sudo pdnsutil retrieve-secondary chans.xyz || sudo systemctl restart pdns sudo pdnsutil retrieve-secondary windy.me || sudo systemctl restart pdns ``` ### 3.4 主节点授权(在 161 的容器上执行) ```bash US1_IP= docker compose exec auth pdnsutil set-meta wsvc.info ALLOW-AXFR-FROM $US1_IP docker compose exec auth pdnsutil set-meta chans.xyz ALLOW-AXFR-FROM $US1_IP docker compose exec auth pdnsutil set-meta windy.me ALLOW-AXFR-FROM $US1_IP # 可选: docker compose exec auth pdnsutil set-meta wsvc.info ALSO-NOTIFY $US1_IP ``` --- ## 4) 常见问题(按本次排障) - **PDA 看不到 zone**:PDA 不读数据库,需连 PDNS API。PDA 服务器配置:`API URL=http://auth:8081`、`API Key` 与 `pdns.conf` 一致、`Server ID=localhost`。从 `pda` 容器内 `curl http://auth:8081/...` 验证。 - **`Empty reply from server`**:`curl` 命中 IPv6 `::1`;改 `curl -4` 或在 `webserver-allow-from` 加 `::1`。 - **`auth` Unhealthy**:多数是 API Key 不一致或健康检查命令在镜像中不可用;改用 `curl` 并确保 key 一致。 - **`Received NOTIFY ... not a primary (Refused)`**:从节点仍为 `MASTER`;将其改为 `SLAVE` 并正确设置 `master`。 --- ## 5) 验收清单(简版) - 主节点:`db` healthy、`auth` API 可返回版本、PDA 通过 NPM 可访问。 - us1:`domains` 中 `type=SLAVE`、`master=154.36.174.161`;`retrieve-secondary` 成功;日志有 AXFR/IXFR 记录。 - 主节点对 `US1_IP` 设置了 `ALLOW-AXFR-FROM`(必要时 `ALSO-NOTIFY`)。 - 关键记录变更已生效(`dig` 结果正确,考虑 TTL 缓存)。 ``` export PGHOST=127.0.0.1 export PGPORT=5432 export PGUSER=pdns export PGPASSWORD='windyboy2006' export PGDATABASE=pdns ``` ``` pg_dump --no-owner --no-acl --format=p --file=~/pdns_backup_$(date +%F).sql \ --single-transaction ``` ``` docker compose exec -T db psql -U pdns -c "DROP DATABASE IF EXISTS pdns;" docker compose exec -T db psql -U pdns -c "CREATE DATABASE pdns OWNER pdns;" ``` ``` docker compose exec -T db psql -U pdns -d template1 -c "DROP DATABASE IF EXISTS pdns;" docker compose exec -T db psql -U pdns -d template1 -c "CREATE DATABASE pdns OWNER pdns;" ``` ``` docker compose exec -T db psql -U pdns -d pdns < backup/pdns_backup_2025-11-03.sql ``` ``` docker compose exec -T db psql -U pdns -d pdns -c "UPDATE domains SET type='MASTER';" ``` ``` docker compose exec -T db psql -U pdns -d pdns -c "DELETE FROM domainmetadata WHERE kind='AXFR-MASTER-TSIG';" ``` ``` docker compose exec -T db psql -U pdns -d pdns -c "TRUNCATE TABLE supermasters;" ``` ``` docker compose exec auth pdnsutil list-all-zones docker compose exec auth pdnsutil check-all-zones docker compose exec auth pdnsutil list-zone windy.me ``` ``` docker compose exec -e PGPASSWORD=windyboy2006 backup psql -h db -U pdns -d pdns -c "\l" ```