# ============================================================================= # docker-compose-combined.yml - every topology in one stack. # # Demonstrates all three PDNS deployment shapes side-by-side so the admin # UI shows them grouped under their backends + cluster: # # 1. Multi-primary cluster - 3 peers sharing one MariaDB backend. # 2. Standalone primary - single PDNS, its own gsqlite3 store. # 3. Primary + 3 Secondaries - classic AXFR replication, supermaster # auto-secondary wiring. # # Plus 10 demo zones × 10 records each per topology = 30 zones / 300 # records seeded by the app's provisioning step (`demo_zones:` section in # provisioning.combined.example.yaml). # # Network: shared `pdnsnet` /24 so the primary's `also-notify` + each # secondary's `supermasters` row can use literal IPs. Cluster peers and # the standalone don't need static IPs. # # Usage: # cp .env.example .env # # fill in APP_SECRET_KEY + APP_ENCRYPTION_KEY # docker compose -f docker-compose-combined.yml up -d # # app → http://localhost:3000 # # cluster peer APIs → http://localhost:8091, :8092, :8093 # # standalone primary API → http://localhost:8095 # # primary+sec primary API → http://localhost:8081 # # primary+sec secondary APIs→ http://localhost:8082, :8083, :8084 # ============================================================================= networks: pdnsnet: driver: bridge ipam: config: - subnet: 172.30.0.0/24 # Dynamic addresses come only from the upper half. The primary and # secondaries pin .10-.13 (their IPs are baked into ALLOW-AXFR / # also-notify), and with two dozen containers dynamic allocation # from .2 upwards reached them first: an intermittent "Address # already in use" at start-up, depending on container order. ip_range: 172.30.0.128/25 name: powerdns-authadmin-combined services: # =========================================================================== # App-side: Postgres + Redis + the Next.js app # =========================================================================== postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: pdns POSTGRES_PASSWORD: pdns POSTGRES_DB: powerdns_authadmin ports: - "5432:5432" volumes: - postgres-data:/var/lib/postgresql/data networks: - pdnsnet healthcheck: test: ["CMD-SHELL", "pg_isready -U pdns -d powerdns_authadmin"] interval: 5s timeout: 5s retries: 10 redis: image: redis:7-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] ports: - "6379:6379" volumes: - redis-data:/data networks: - pdnsnet healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 10 # =========================================================================== # Topology 1: Multi-primary cluster (3 peers + shared MariaDB) # =========================================================================== mariadb: image: mariadb:11 restart: unless-stopped environment: MARIADB_ROOT_PASSWORD: pdns-root-pw MARIADB_DATABASE: pdns MARIADB_USER: pdns MARIADB_PASSWORD: pdns-pw volumes: - mariadb-data:/var/lib/mysql - ./docker/pdns/schema.mysql.sql:/docker-entrypoint-initdb.d/01-pdns-schema.sql:ro networks: - pdnsnet healthcheck: # Over TCP, not the socket: on first boot the image runs a socket-only # temporary server while it loads the schema, and a socket check passes # against it before PowerDNS can connect over the network. test: ["CMD-SHELL", "mariadb -h 127.0.0.1 -updns -ppdns-pw -e 'SELECT 1' pdns"] interval: 5s timeout: 5s retries: 20 pdns-peer-1: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: mariadb: condition: service_healthy ports: - "8091:8081" networks: - pdnsnet volumes: - ./docker/pdns/peer.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=peer-1-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s pdns-peer-2: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: mariadb: condition: service_healthy ports: - "8092:8081" networks: - pdnsnet volumes: - ./docker/pdns/peer.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=peer-2-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s pdns-peer-3: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: mariadb: condition: service_healthy ports: - "8093:8081" networks: - pdnsnet volumes: - ./docker/pdns/peer.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=peer-3-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s # =========================================================================== # Topology 2: Standalone primary (single PDNS, no replication) # =========================================================================== pdns-single-init: image: alpine:3.20 restart: "no" command: - sh - -c - | set -eu apk add --no-cache sqlite >/dev/null sh /init.sh chown -R 953:953 /var/lib/powerdns chmod -R u+rwX /var/lib/powerdns environment: DB: /var/lib/powerdns/pdns.sqlite SCHEMA: /schema.sqlite3.sql volumes: - single-data:/var/lib/powerdns - ./docker/pdns/init-primary.sh:/init.sh:ro - ./docker/pdns/schema.sqlite3.sql:/schema.sqlite3.sql:ro networks: - pdnsnet pdns-single: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: pdns-single-init: condition: service_completed_successfully ports: - "5310:53" - "5310:53/udp" - "8095:8081" networks: - pdnsnet volumes: - single-data:/var/lib/powerdns - ./docker/pdns/single.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=single-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s # =========================================================================== # Topology 3: Primary + 3 Secondaries (NOTIFY + AXFR replication) # =========================================================================== pdns-ps-primary-init: image: alpine:3.20 restart: "no" command: - sh - -c - | set -eu apk add --no-cache sqlite >/dev/null sh /init.sh chown -R 953:953 /var/lib/powerdns chmod -R u+rwX /var/lib/powerdns environment: DB: /var/lib/powerdns/pdns.sqlite SCHEMA: /schema.sqlite3.sql volumes: - ps-primary-data:/var/lib/powerdns - ./docker/pdns/init-primary.sh:/init.sh:ro - ./docker/pdns/schema.sqlite3.sql:/schema.sqlite3.sql:ro networks: - pdnsnet pdns-ps-primary: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: pdns-ps-primary-init: condition: service_completed_successfully ports: - "5300:53" - "5300:53/udp" - "8081:8081" networks: pdnsnet: ipv4_address: 172.30.0.10 volumes: - ps-primary-data:/var/lib/powerdns - ./docker/pdns/primary.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=ps-primary-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s pdns-ps-secondary-1-init: image: alpine:3.20 restart: "no" command: - sh - -c - | set -eu apk add --no-cache sqlite >/dev/null sh /init.sh chown -R 953:953 /var/lib/powerdns chmod -R u+rwX /var/lib/powerdns environment: DB: /var/lib/powerdns/pdns.sqlite SCHEMA: /schema.sqlite3.sql PRIMARY_IPS: "172.30.0.10" # PDNS auto-secondary verifies the supermasters row's `nameserver` # column against the NS records the Primary advertises for the # zone. Each Secondary registers itself under its own container # hostname so the verification doesn't reject the auto-create. SELF_NS: "pdns-ps-secondary-1" volumes: - ps-secondary-1-data:/var/lib/powerdns - ./docker/pdns/init-secondary.sh:/init.sh:ro - ./docker/pdns/schema.sqlite3.sql:/schema.sqlite3.sql:ro networks: - pdnsnet pdns-ps-secondary-1: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: pdns-ps-secondary-1-init: condition: service_completed_successfully ports: - "5301:53" - "5301:53/udp" - "8082:8081" networks: pdnsnet: ipv4_address: 172.30.0.11 volumes: - ps-secondary-1-data:/var/lib/powerdns - ./docker/pdns/secondary.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=ps-secondary-1-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s pdns-ps-secondary-2-init: image: alpine:3.20 restart: "no" command: - sh - -c - | set -eu apk add --no-cache sqlite >/dev/null sh /init.sh chown -R 953:953 /var/lib/powerdns chmod -R u+rwX /var/lib/powerdns environment: DB: /var/lib/powerdns/pdns.sqlite SCHEMA: /schema.sqlite3.sql PRIMARY_IPS: "172.30.0.10" SELF_NS: "pdns-ps-secondary-2" volumes: - ps-secondary-2-data:/var/lib/powerdns - ./docker/pdns/init-secondary.sh:/init.sh:ro - ./docker/pdns/schema.sqlite3.sql:/schema.sqlite3.sql:ro networks: - pdnsnet pdns-ps-secondary-2: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: pdns-ps-secondary-2-init: condition: service_completed_successfully ports: - "5302:53" - "5302:53/udp" - "8083:8081" networks: pdnsnet: ipv4_address: 172.30.0.12 volumes: - ps-secondary-2-data:/var/lib/powerdns - ./docker/pdns/secondary.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=ps-secondary-2-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s pdns-ps-secondary-3-init: image: alpine:3.20 restart: "no" command: - sh - -c - | set -eu apk add --no-cache sqlite >/dev/null sh /init.sh chown -R 953:953 /var/lib/powerdns chmod -R u+rwX /var/lib/powerdns environment: DB: /var/lib/powerdns/pdns.sqlite SCHEMA: /schema.sqlite3.sql PRIMARY_IPS: "172.30.0.10" SELF_NS: "pdns-ps-secondary-3" volumes: - ps-secondary-3-data:/var/lib/powerdns - ./docker/pdns/init-secondary.sh:/init.sh:ro - ./docker/pdns/schema.sqlite3.sql:/schema.sqlite3.sql:ro networks: - pdnsnet pdns-ps-secondary-3: image: ${PDNS_AUTH_IMAGE:-powerdns/pdns-auth-49:latest} restart: unless-stopped depends_on: pdns-ps-secondary-3-init: condition: service_completed_successfully ports: - "5303:53" - "5303:53/udp" - "8084:8081" networks: pdnsnet: ipv4_address: 172.30.0.13 volumes: - ps-secondary-3-data:/var/lib/powerdns - ./docker/pdns/secondary.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=ps-secondary-3-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s # =========================================================================== # The app # =========================================================================== app: build: context: . dockerfile: Dockerfile target: runner restart: unless-stopped depends_on: postgres: condition: service_healthy redis: condition: service_healthy pdns-peer-1: condition: service_healthy pdns-peer-2: condition: service_healthy pdns-peer-3: condition: service_healthy pdns-single: condition: service_healthy pdns-ps-primary: condition: service_healthy pdns-ps-secondary-1: condition: service_healthy pdns-ps-secondary-2: condition: service_healthy pdns-ps-secondary-3: condition: service_healthy ports: - "3000:3000" networks: - pdnsnet volumes: - sqlite-data:/data - ./provisioning.combined.example.yaml:/etc/powerdns-authadmin/provisioning.yaml:ro environment: NODE_ENV: ${NODE_ENV:-production} PORT: 3000 APP_URL: ${APP_URL:-http://localhost:3000} LOG_LEVEL: ${LOG_LEVEL:-info} # Postgres-backed: the integration suite (host-side vitest) connects to # this same Postgres over the published 5432 to reset state, so the app # must use it (an in-container SQLite file isn't reachable from the host). DATABASE_URL: postgres://pdns:pdns@postgres:5432/powerdns_authadmin REDIS_URL: redis://redis:6379 APP_SECRET_KEY: ${APP_SECRET_KEY:?must be set; generate with openssl rand -base64 32} APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?must be set; generate with openssl rand -base64 32} BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL:-admin@example.com} BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:-bootstrap-admin-pw-changeme} # This is the public demo stack: lock the shared admin's login so a # visitor can't change the password/email/MFA or delete it out from under # everyone. Override BOOTSTRAP_ADMIN_RO=false for a private deployment. BOOTSTRAP_ADMIN_RO: ${BOOTSTRAP_ADMIN_RO:-true} LOCAL_AUTH_ENABLED: "true" APP_PDNS_ALLOW_PRIVATE_NETWORKS: "true" # Enable the background poller so the dashboard's PDNS-statistics tab # (per-backend query rate / latency / cache / response charts) renders - # it's hidden when polling is off. Also what the demo graph backfill # (scripts/demo-seed-graphs.ts) populates. PDNS_BACKGROUND_POLLING: ${PDNS_BACKGROUND_POLLING:-true} MIGRATE_ON_BOOT: ${MIGRATE_ON_BOOT:-true} SEED_ON_BOOT: ${SEED_ON_BOOT:-true} PROVISIONING_FILE: /etc/powerdns-authadmin/provisioning.yaml PROVISION_ON_BOOT: ${PROVISION_ON_BOOT:-true} APP_PDNS_ALLOW_INSECURE_HTTP: ${APP_PDNS_ALLOW_INSECURE_HTTP:-true} healthcheck: test: [ "CMD", "node", "-e", "fetch('http://localhost:3000/healthz').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))", ] interval: 15s timeout: 5s retries: 5 start_period: 30s volumes: postgres-data: redis-data: mariadb-data: single-data: ps-primary-data: ps-secondary-1-data: ps-secondary-2-data: ps-secondary-3-data: sqlite-data: