Files
my-vault/01_Projects/Infrastructure/VPS/DNS.md
T

18 KiB
Raw Blame History

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 建表”的方案)

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.confPostgreSQL 后端示例)

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) 启动

docker compose up -d
# 等 db 健康检查 OK 后,auth 会启动并可通过 8081 API 访问

手动恢复脚本(支持 .sql 与 -Fc)

scripts/restore-pdns.sh:默认安全模式是恢复到一个新数据库(避免与你现有 schema 冲突),完成后你只需把 pdns.confgpgsql-dbname 改成新库名并 docker compose restart auth 即可。也提供 --inplace 选项可“原地覆盖”(会清空原库 public 模式)——谨慎使用。

#!/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] <path-to-dump>"
  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

给执行权限:

chmod +x scripts/restore-pdns.sh

使用示例

  1. 默认安全模式(恢复到新库,不影响当前运行):
./scripts/restore-pdns.sh ./backup/pdns.dump
# 按提示把 auth/pdns.conf 的 gpgsql-dbname 改为脚本输出的新库名
docker compose restart auth
  1. 原地覆盖(会清空当前库的 public schema):
./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

-- 创建 PowerDNS-Admin 的数据库与用户(与 PDNS 库隔离)
CREATE USER pdnsadmin WITH PASSWORD 'windyboy2006';
CREATE DATABASE pdnsadmin OWNER pdnsadmin ENCODING 'UTF8';
GRANT ALL PRIVILEGES ON DATABASE pdnsadmin TO pdnsadmin;

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

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();"
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 — 简版操作手册(按本次实际操作)

场景回顾:主节点(Docker154.36.174.161)更换/确认公网 IP;二级节点 us1.wsvc.info 为二进制安装(非 Docker)。前端用 Nginx Proxy Manager (NPM) 代理 PowerDNS-Admin (PDA)PDNS API


1) 现状与目标

  • 主节点(Dockerpdns-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/udpPDA 与 PDNS API 仅在容器网络内,由 NPM 反代并做 IP 白名单。


2) 主节点(Docker)操作

2.1 网络与 Compose

docker network create npm_proxy || true

pdns compose 关键点:

  • auth

    • ports: 只保留 53:53/udp53:53/tcp

    • expose: 8081API 仅在容器网络可见)。

    • networks: 加入 default + npm_proxy

    • 挂载目录./auth:/etc/powerdns:ro(避免 pdns.conf 丢失)。

    • 健康检查可用 curlcurl -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 关键参数

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

    • PDApda:80(绑定域名,Access List 白名单)。

    • PDNS APIauth:8081(同上)。

在 NPM 容器内自检:

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
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
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 +shortdig @127.0.0.1 hk2.chans.xyz A +short

3) us1(二进制从节点)操作(本次实际)

us1 使用二进制 pdns-server,数据库 domains 为旧 schematype/master)。本次已验证以下两种方式皆可;推荐优先使用 pdnsutil 命令

3.1 推荐:用 pdnsutil 指向新主并拉取

确保允许从区:

# /etc/powerdns/pdns.conf
secondary=yes   # 旧版本为 slave=yes

把各区改为 Secondary,并设置新的主(154.36.174.161),然后触发 AXFR

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

日志与连通性:

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:没有该子命令时,用“重建从区”

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:直接改数据库后补拉取(你本次已用)

-- 在 us1 上
UPDATE domains SET type='SLAVE', master='154.36.174.161'
 WHERE name IN ('wsvc.info','chans.xyz','windy.me');

然后:

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 的容器上执行)

US1_IP=<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:8081API Keypdns.conf 一致、Server ID=localhost。从 pda 容器内 curl http://auth:8081/... 验证。

  • Empty reply from servercurl 命中 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 可访问。

  • us1domainstype=SLAVEmaster=154.36.174.161retrieve-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"