# ============================================================================= # docker-compose-primary-secondaries.yml - full demo stack with 1 PowerDNS # Primary + 3 PowerDNS Secondaries. # # Showcases the app's sync-state surface end-to-end: writes go to the Primary, # PDNS' built-in NOTIFY + AXFR replicates each zone to every Secondary, and # the app's per-secondary "in-sync / lagging" probe lights up green once the # Secondaries catch up. # # Design: # * Plain `powerdns/pdns-auth:latest` for every PDNS container - no custom # image, no Dockerfile in this stack. # * Each container mounts a role-specific conf from `docker/pdns/*.conf` # and the gsqlite3 schema from `docker/pdns/schema.sqlite3.sql`. # * A tiny init container per PDNS role uses `alpine` + `sqlite3` to # apply the schema (and, for Secondaries, insert the supermaster row). # Main container depends on `service_completed_successfully`. # * Static IPs on a /24 so the Primary's `also-notify` + each Secondary's # `supermasters` row can use literal IPs - PDNS doesn't accept CIDRs # in those positions. # # Usage: # cp .env.example .env # # fill in APP_SECRET_KEY + APP_ENCRYPTION_KEY # docker compose -f docker-compose-primary-secondaries.yml up -d # # app → http://localhost:3000 # # Primary API → http://localhost:8081 (X-API-Key: primary-changeme) # # Secondary APIs → http://localhost:8082, :8083, :8084 # ============================================================================= networks: pdnsnet: driver: bridge ipam: config: - subnet: 172.30.0.0/24 name: powerdns-authadmin-primary-secondaries services: # ---- PostgreSQL + Redis (app side) ----------------------------------- 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 # ---- PDNS Primary ---------------------------------------------------- pdns-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: - 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-primary: image: powerdns/pdns-auth-49:latest restart: unless-stopped depends_on: pdns-primary-init: condition: service_completed_successfully ports: - "5300:53" - "5300:53/udp" - "8081:8081" networks: pdnsnet: ipv4_address: 172.30.0.10 volumes: - primary-data:/var/lib/powerdns - ./docker/pdns/primary.conf:/etc/powerdns/pdns.conf:ro # Override the api-key via CLI so the conf file stays generic. command: ["--api-key=primary-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s # ---- PDNS Secondary 1 ------------------------------------------------ pdns-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" SELF_NS: "pdns-secondary-1" volumes: - 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-secondary-1: image: powerdns/pdns-auth-49:latest restart: unless-stopped depends_on: pdns-secondary-1-init: condition: service_completed_successfully ports: - "5301:53" - "5301:53/udp" - "8082:8081" networks: pdnsnet: ipv4_address: 172.30.0.11 volumes: - secondary-1-data:/var/lib/powerdns - ./docker/pdns/secondary.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=secondary-1-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s # ---- PDNS Secondary 2 ------------------------------------------------ pdns-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-secondary-2" volumes: - 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-secondary-2: image: powerdns/pdns-auth-49:latest restart: unless-stopped depends_on: pdns-secondary-2-init: condition: service_completed_successfully ports: - "5302:53" - "5302:53/udp" - "8083:8081" networks: pdnsnet: ipv4_address: 172.30.0.12 volumes: - secondary-2-data:/var/lib/powerdns - ./docker/pdns/secondary.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=secondary-2-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s # ---- PDNS Secondary 3 ------------------------------------------------ pdns-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-secondary-3" volumes: - 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-secondary-3: image: powerdns/pdns-auth-49:latest restart: unless-stopped depends_on: pdns-secondary-3-init: condition: service_completed_successfully ports: - "5303:53" - "5303:53/udp" - "8084:8081" networks: pdnsnet: ipv4_address: 172.30.0.13 volumes: - secondary-3-data:/var/lib/powerdns - ./docker/pdns/secondary.conf:/etc/powerdns/pdns.conf:ro command: ["--api-key=secondary-3-changeme"] healthcheck: test: ["CMD", "pdns_control", "rping"] interval: 10s timeout: 5s retries: 5 start_period: 10s # ---- The app --------------------------------------------------------- # Boots with PROVISIONING_FILE pointing at the bundled provisioning YAML, # which pre-registers all four PDNS backends and a starter zone template. app: build: context: . dockerfile: Dockerfile target: runner restart: unless-stopped depends_on: postgres: condition: service_healthy redis: condition: service_healthy pdns-primary: condition: service_healthy pdns-secondary-1: condition: service_healthy pdns-secondary-2: condition: service_healthy pdns-secondary-3: condition: service_healthy ports: - "3000:3000" networks: - pdnsnet volumes: - ./provisioning.primary-secondary.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} 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} LOCAL_AUTH_ENABLED: "true" APP_PDNS_ALLOW_PRIVATE_NETWORKS: "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: primary-data: secondary-1-data: secondary-2-data: secondary-3-data: