User Tools

Site Tools


docker:docker-compose:powerdns

PowerDNS

Single instance - Master only

Create network for powerdns
docker network create --ipv4 -d bridge \
--gateway 192.168.156.1 \
--subnet 192.168.156.0/24 \
powerdns_default
pdns/pdns.conf
local-address=0.0.0.0,::
local-port=53

launch=gpgsql
gpgsql-host=db
gpgsql-port=5432
gpgsql-dbname=pdns
gpgsql-user=pdns
gpgsql-password=pdns_strong_password

# Optional but recommended if you will use DNSSEC with this backend:
gpgsql-dnssec=yes
# The docs note DNSSEC is enabled for this backend by setting gpgsql-dnssec. [6](https://doc.powerdns.com/authoritative/backends/generic-postgresql.html)

include-dir=/etc/powerdns/pdns.d

# Good container defaults
daemon=no
disable-syslog=yes
loglevel=4

trusted-notification-proxy=192.168.156.11
allow-axfr-ips=192.168.156.11
pdns.d/api.conf
# Enable API + webserver
webserver=yes
api=yes

# Bind API on all interfaces (because it's a container), but RESTRICT who can reach it:
webserver-address=0.0.0.0
webserver-port=8081
webserver-allow-from=127.0.0.1/32,::1/128,192.168.0.0/16

# Authentication
api-key=change_me_long_random_key
webserver-password=change_me_another_password
docker-compose.yaml
services:
  db:
    image: postgres:16
    container_name: pdns-db
    environment:
      POSTGRES_DB: pdns
      POSTGRES_USER: pdns
      POSTGRES_PASSWORD: pdns_strong_password
    networks:
      powerdns_default:
        ipv4_address: 192.168.156.9
    volumes:
      - pdns_pgdata:/var/lib/postgresql/data
      - ./postgres-init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U pdns -d pdns"]
      interval: 5s
      timeout: 3s
      retries: 20

  pdns:
    image: powerdns/pdns-auth-49
    container_name: pdns-auth
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:5300:5300/tcp"
      - "127.0.0.1:5300:5300/udp"
      - "192.168.22.8:8089:8081/tcp"
    networks:
      powerdns_default:
        ipv4_address: 192.168.156.10
    volumes:
      - ./pdns/pdns.conf:/etc/powerdns/pdns.conf:ro
      - ./pdns.d:/etc/powerdns/pdns.d:rw
    restart: unless-stopped

  recursor:
    image: powerdns/pdns-recursor-55
    container_name: pdns-recursor
    depends_on:
      - pdns
    ports:
      - "192.168.22.8:53:53/udp"
      - "192.168.22.8:53:53/tcp"
    networks:
      powerdns_default:
        ipv4_address: 192.168.156.11
    command:
      - "--local-address=0.0.0.0"
      - "--local-port=53"
      - "--allow-from=192.168.22.0/24,127.0.0.1/32,192.168.156.0/24"
      - "--forward-zones=example.com=192.168.156.10:53"
      - "--forward-zones-recurse=.=8.8.8.8;1.1.1.1"
      - "--dont-query=127.0.0.0/8,::1/128"
      - "--log-common-errors=yes"
    restart: unless-stopped


volumes:
  pdns_pgdata:

networks:
  powerdns_default:
    external: true
database schema.sql
CREATE TABLE domains (
  id                    SERIAL PRIMARY KEY,
  name                  VARCHAR(255) NOT NULL,
  master                VARCHAR(128) DEFAULT NULL,
  last_check            INT DEFAULT NULL,
  type                  TEXT NOT NULL,
  notified_serial       BIGINT DEFAULT NULL,
  account               VARCHAR(40) DEFAULT NULL,
  options               TEXT DEFAULT NULL,
  catalog               TEXT DEFAULT NULL,
  CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);

CREATE UNIQUE INDEX name_index ON domains(name);
CREATE INDEX catalog_idx ON domains(catalog);


CREATE TABLE records (
  id                    BIGSERIAL PRIMARY KEY,
  domain_id             INT DEFAULT NULL,
  name                  VARCHAR(255) DEFAULT NULL,
  type                  VARCHAR(10) DEFAULT NULL,
  content               VARCHAR(65535) DEFAULT NULL,
  ttl                   INT DEFAULT NULL,
  prio                  INT DEFAULT NULL,
  disabled              BOOL DEFAULT 'f',
  ordername             VARCHAR(255),
  auth                  BOOL DEFAULT 't',
  CONSTRAINT domain_exists
  FOREIGN KEY(domain_id) REFERENCES domains(id)
  ON DELETE CASCADE,
  CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);

CREATE INDEX rec_name_index ON records(name);
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
CREATE INDEX recordorder ON records (domain_id, ordername text_pattern_ops);


CREATE TABLE supermasters (
  ip                    INET NOT NULL,
  nameserver            VARCHAR(255) NOT NULL,
  account               VARCHAR(40) NOT NULL,
  PRIMARY KEY(ip, nameserver)
);


CREATE TABLE comments (
  id                    SERIAL PRIMARY KEY,
  domain_id             INT NOT NULL,
  name                  VARCHAR(255) NOT NULL,
  type                  VARCHAR(10) NOT NULL,
  modified_at           INT NOT NULL,
  account               VARCHAR(40) DEFAULT NULL,
  comment               VARCHAR(65535) NOT NULL,
  CONSTRAINT domain_exists
  FOREIGN KEY(domain_id) REFERENCES domains(id)
  ON DELETE CASCADE,
  CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);

CREATE INDEX comments_domain_id_idx ON comments (domain_id);
CREATE INDEX comments_name_type_idx ON comments (name, type);
CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);


CREATE TABLE domainmetadata (
  id                    SERIAL PRIMARY KEY,
  domain_id             INT REFERENCES domains(id) ON DELETE CASCADE,
  kind                  VARCHAR(32),
  content               TEXT
);

CREATE INDEX domainidmetaindex ON domainmetadata(domain_id);


CREATE TABLE cryptokeys (
  id                    SERIAL PRIMARY KEY,
  domain_id             INT REFERENCES domains(id) ON DELETE CASCADE,
  flags                 INT NOT NULL,
  active                BOOL,
  published             BOOL DEFAULT TRUE,
  content               TEXT
);

CREATE INDEX domainidindex ON cryptokeys(domain_id);


CREATE TABLE tsigkeys (
  id                    SERIAL PRIMARY KEY,
  name                  VARCHAR(255),
  algorithm             VARCHAR(50),
  secret                VARCHAR(255),
  CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);

CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);
Load the schema
docker exec -it pdns-db psql -U pdns -d pdns
***PASTE the SCHEMA ABOVE***

#Show the tables:
pdns=# \dt
            List of relations
 Schema |      Name      | Type  | Owner 
--------+----------------+-------+-------
 public | comments       | table | pdns
 public | cryptokeys     | table | pdns
 public | domainkeys     | table | pdns
 public | domainmetadata | table | pdns
 public | domains        | table | pdns
 public | records        | table | pdns
 public | supermasters   | table | pdns
 public | tsigkeys       | table | pdns
(8 rows)
add entries
docker compose exec pdns pdnsutil create-zone example.com ns1.example.com
docker compose exec pdns pdnsutil add-record example.com example.com NS ns2.example.com
docker compose exec pdns pdnsutil add-record example.com example.com NS ns3.example.com

docker compose exec pdns pdnsutil replace-rrset example.com '' SOA 3600 "ns1.example.com. hostmaster.example.com. 1 10800 3600 604800 3600"

docker compose exec pdns pdnsutil add-record example.com ns1 A 192.168.10.11 && \
docker compose exec pdns pdnsutil add-record example.com ns2 A 192.168.10.12 && \
docker compose exec pdns pdnsutil add-record example.com ns3 A 192.168.10.13 && \
docker compose exec pdns pdnsutil add-record example.com ns1 AAAA 2001:db8::11 && \
docker compose exec pdns pdnsutil add-record example.com ns2 AAAA 2001:db8::12 && \
docker compose exec pdns pdnsutil add-record example.com ns3 AAAA 2001:db8::13 && \
docker compose exec pdns pdnsutil add-record example.com www A 3600 "192.168.10.51"

docker compose exec pdns pdnsutil increase-serial example.com

docker compose exec pdns pdnsutil list-zone example.com

GUI/API

To access the GUI (http://192.168.22.8:8089/), you must use the admin username and the password defined in “pdns/pdns.conf”.

For API access, the API secret key is also defined in “pdns/pdns.conf”.

jonathan@jonathan-VirtualBox:~/Downloads$ curl -H "X-API-Key: change_me_long_random_key"      http://192.168.22.8:8089/api/v1/servers | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   328  100   328    0     0  50719      0 --:--:-- --:--:-- --:--:-- 54666
[
  {
    "autoprimaries_url": "/api/v1/servers/localhost/autoprimaries{/autoprimary}",
    "config_url": "/api/v1/servers/localhost/config{/config_setting}",
    "daemon_type": "authoritative",
    "id": "localhost",
    "type": "Server",
    "url": "/api/v1/servers/localhost",
    "version": "4.9.13",
    "zones_url": "/api/v1/servers/localhost/zones{/zone}"
  }
]



jonathan@jonathan-VirtualBox:~/Downloads$ curl -s \
  -H "X-API-Key: change_me_long_random_key" \
  http://192.168.22.8:8089/api/v1/servers/localhost/zones \
  | jq .
[
  {
    "account": "",
    "catalog": "",
    "dnssec": false,
    "edited_serial": 2,
    "id": "example.com.",
    "kind": "Native",
    "last_check": 0,
    "masters": [],
    "name": "example.com.",
    "notified_serial": 0,
    "serial": 2,
    "url": "/api/v1/servers/localhost/zones/example.com."
  }
]



jonathan@jonathan-VirtualBox:~/Downloads$ curl -s \
  -H "X-API-Key: change_me_long_random_key" \
  http://192.168.22.8:8089/api/v1/servers/localhost/zones/example.com. \
  | jq .
{
  "account": "",
  "api_rectify": false,
  "catalog": "",
  "dnssec": false,
  "edited_serial": 2,
  "id": "example.com.",
  "kind": "Native",
  "last_check": 0,
  "master_tsig_key_ids": [],
  "masters": [],
  "name": "example.com.",
  "notified_serial": 0,
  "nsec3narrow": false,
  "nsec3param": "",
  "rrsets": [
    {
      "comments": [],
      "name": "www.example.com.",
      "records": [
        {
          "content": "192.168.10.51",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "A"
    },
    {
      "comments": [],
      "name": "mail.example.com.",
      "records": [
        {
          "content": "192.168.10.60",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "A"
    },
    {
      "comments": [],
      "name": "ns2.example.com.",
      "records": [
        {
          "content": "192.168.22.8",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "A"
    },
    {
      "comments": [],
      "name": "ns1.example.com.",
      "records": [
        {
          "content": "192.168.22.8",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "A"
    },
    {
      "comments": [],
      "name": "example.com.",
      "records": [
        {
          "content": "\"v=spf1 -all\"",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "TXT"
    },
    {
      "comments": [],
      "name": "example.com.",
      "records": [
        {
          "content": "10 mail.example.com.",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "MX"
    },
    {
      "comments": [],
      "name": "example.com.",
      "records": [
        {
          "content": "ns1.example.com. hostmaster.example.com. 2 10800 3600 604800 3600",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "SOA"
    },
    {
      "comments": [],
      "name": "example.com.",
      "records": [
        {
          "content": "ns1.example.com.",
          "disabled": false
        },
        {
          "content": "ns2.example.com.",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "NS"
    },
    {
      "comments": [],
      "name": "example.com.",
      "records": [
        {
          "content": "192.168.10.50",
          "disabled": false
        }
      ],
      "ttl": 3600,
      "type": "A"
    }
  ],
  "serial": 2,
  "slave_tsig_key_ids": [],
  "soa_edit": "",
  "soa_edit_api": "",
  "url": "/api/v1/servers/localhost/zones/example.com."
}





jonathan@jonathan-VirtualBox:~/Downloads$ curl -s \
  -H "X-API-Key: change_me_long_random_key" \
  http://192.168.22.8:8089/api/v1/servers/localhost/zones/example.com. \
  | jq '.rrsets'
[
  {
    "comments": [],
    "name": "www.example.com.",
    "records": [
      {
        "content": "192.168.10.51",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "A"
  },
  {
    "comments": [],
    "name": "mail.example.com.",
    "records": [
      {
        "content": "192.168.10.60",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "A"
  },
  {
    "comments": [],
    "name": "ns2.example.com.",
    "records": [
      {
        "content": "192.168.22.8",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "A"
  },
  {
    "comments": [],
    "name": "ns1.example.com.",
    "records": [
      {
        "content": "192.168.22.8",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "A"
  },
  {
    "comments": [],
    "name": "example.com.",
    "records": [
      {
        "content": "\"v=spf1 -all\"",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "TXT"
  },
  {
    "comments": [],
    "name": "example.com.",
    "records": [
      {
        "content": "10 mail.example.com.",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "MX"
  },
  {
    "comments": [],
    "name": "example.com.",
    "records": [
      {
        "content": "ns1.example.com. hostmaster.example.com. 2 10800 3600 604800 3600",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "SOA"
  },
  {
    "comments": [],
    "name": "example.com.",
    "records": [
      {
        "content": "ns1.example.com.",
        "disabled": false
      },
      {
        "content": "ns2.example.com.",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "NS"
  },
  {
    "comments": [],
    "name": "example.com.",
    "records": [
      {
        "content": "192.168.10.50",
        "disabled": false
      }
    ],
    "ttl": 3600,
    "type": "A"
  }
]
docker/docker-compose/powerdns.txt · Last modified: by jonathan