DNSGeo and PowerDNS
Desired-State Reconciliation
Core generates all durable customer and edge rrsets into desired_dns_rrsets. Event-driven changes and the dns-reconciler scheduler both run php artisan cdn:dns:reconcile. A PostgreSQL advisory lock prevents overlapping runs. Each run compares desired state with the live PowerDNS zone, sends one batch PATCH per changed zone, verifies the write, and deletes rrsets that were previously owned by CDNLite but are no longer desired.
Every domain saved in Core is part of desired DNS immediately. Domain mutations save Core state quickly and queue the reconciler to create or prune the authority-only zone. Before nameserver verification succeeds, that customer zone contains only CDNLite's platform NS plus the PowerDNS-created SOA. Users can create DNS records during onboarding, but those records stay in PostgreSQL and are not published until the domain is active and its authoritative nameservers verify against the configured platform nameservers. If verification later fails because the domain was delegated away, Core keeps the domain and all DNS records in PostgreSQL, marks the domain pending, and reconciliation withdraws every non-NS/SOA rrset from PowerDNS. Deleting the domain removes the whole PowerDNS zone, including the automatically generated SOA.
Domain DNS record list APIs and the dashboard always show the platform NS records as readonly managed rows. They come from current platform nameserver settings, not from editable customer DNS records.
Use CDNLITE_SYNC_INTERVAL_SECONDS to set the scheduled convergence interval (default: 30). php artisan cdn:powerdns:dry-run previews desired rrsets, while php artisan cdn:powerdns:force-sync forces a full replacement pass.
Every managed PowerDNS zone also gets one apex SOA record owned by platform authority settings, not by customer DNS records. Configure it with CDNLITE_DNS_PRIMARY_NS, CDNLITE_DNS_HOSTMASTER, CDNLITE_DNS_SOA_REFRESH, CDNLITE_DNS_SOA_RETRY, CDNLITE_DNS_SOA_EXPIRE, CDNLITE_DNS_SOA_MINIMUM, and CDNLITE_DNS_SOA_TTL. Core keeps a monotonic per-zone serial and only increments it when the managed zone content changes.
The root docker-compose.yml starts the project DNS stack by default:
pdns-postgres: PostgreSQL backend dedicated to DNS data.pdns-db-init: idempotent PowerDNS and Poweradmin schema initialization.pdns-mmdb-updater: GeoIP MMDB downloader and freshness loop.pdns-recursor: recursive resolver kept available for local recursive checks.pdns-auth: PowerDNS Authoritative 5.x with PostgreSQL, Lua records, and EDNS Client Subnet.poweradmin: operator UI backed by the same DNS database.
Start the product with:
docker compose up -d --buildThe API and Poweradmin ports bind to 127.0.0.1 by default. Do not publish the PowerDNS API directly to the internet. Put Poweradmin behind authenticated HTTPS or a VPN before remote use.
The authoritative service uses restart: unless-stopped. The MMDB watcher intentionally terminates PowerDNS after an atomic database replacement so Docker restarts it and remaps the new file.
Resolver Support
PowerDNS Authoritative is rendered with:
expand-alias=yes
resolver=pdns-recursor:5300The resolver is a separate process. It forwards the configured CDNLITE_DNS_BASE_DOMAIN and CDNLITE_CDN_ZONE to pdns-auth; it never points recursive traffic back to the authoritative server as a general resolver. Proxied customer apex records do not use ALIAS; they publish direct managed apex A/AAAA answers from static anycast settings or PowerDNS LUA edge-pool records.
DNSSEC signing is not enabled by the bundled fresh-install defaults. Operators enabling DNSSEC must validate Lua answers, parent DS publication, and any manually added ALIAS records before production rollout.
Configuration
The bundled service is provisioned with:
PDNS_API_KEY=change-me
CDNLITE_DNS_BASE_DOMAIN=example.net
CDNLITE_CDN_ZONE=cdn.example.net
CDNLITE_CDN_PROXY_HOST=proxy.cdn.example.netShared Edge Pool
Core reads the fresh-install edge_state view as the single DNS routing source. An address is eligible only when its edge is enabled for DNS, online, healthy, and has a heartbeat newer than 90 seconds. Anycast addresses are ordered before regional unicast addresses. Core keeps edge ordering from edge_state, writes each target IP with the edge country and continent into the PowerDNS Lua record, and uses the first eligible edge IP as the final fallback answer.
The CDN zone contains one shared pair of Lua rrsets:
proxy.cdn.example.net. LUA A <healthy IPv4 pool>
proxy.cdn.example.net. LUA AAAA <healthy IPv6 pool>Admins can override either address family from the dashboard under Settings -> Edge DNS / Anycast by setting anycast_ipv4 and/or anycast_ipv6. Each setting accepts one or more comma-, space-, or newline-separated IP addresses, such as 203.0.113.10, 203.0.113.11. When static anycast IPs are configured, the shared proxy host publishes plain A or AAAA rrsets containing all configured addresses for that family and completely bypasses DNSGeo Lua, country routing, and continent routing for that family. CDNLite keeps proxied subdomains on stable CNAME targets, while proxied apex records publish from the same canonical edge-pool renderer as the shared proxy host: direct A/AAAA when static anycast is configured, and PowerDNS LUA otherwise.
Stable site-<domain-id>.cdn.example.net CNAMEs point to that shared host for proxied subdomains. Proxied customer apex records are always published directly at the zone apex, never as ALIAS or CNAME. Static anycast settings publish normal managed apex A/AAAA; otherwise CDNLite publishes PowerDNS LUAA/AAAA content. DNS-only apex A and AAAA records remain normal address records. Changing an edge IP, health state, or static anycast setting updates the shared proxy records and every managed proxied apex record through reconciliation. Edge heartbeat and registration requests trigger reconciliation only when the effective DNS edge pool changes. Each generation records distinct edge-state hashes in edge_state_generations for inspection and test assertions.
For DNS-only A and AAAA records, content must remain an IPv4 or IPv6 address respectively. For proxied A and AAAA records, the dashboard content field is the private default origin and accepts either an IP address or a hostname. Proxy origin overrides are edge proxy configuration and do not change the public LUA/CNAME records.
Raw GeoDNS Records
Raw GeoDNS is a third DNS record mode, separate from normal DNS-only records and proxied CDN records. It is currently supported for DNS-only A and AAAA records. Users enter the record type, name, required default answer, TTL, and optional country or continent answers. Users never enter Lua.
Core stores these answers in dns_record_geo_routes and publishes one PowerDNS LUA rrset for the record. Generated Lua checks country routes first, then continent routes, then returns the default answer. Country codes are ISO 3166-1 alpha-2 values, and continent codes are AF, AN, AS, EU, NA, OC, and SA.
Proxy and raw GeoDNS routes are mutually exclusive at the record level. Enabling proxy removes explicit raw GeoDNS routes, but the default domain routing mode does not make every proxied record a GeoDNS record. Attempts to save proxy plus country or continent GeoDNS routes are rejected with proxy_and_geodns_are_mutually_exclusive. CNAME GeoDNS is disabled because the current supported raw GeoDNS publisher only emits safe A/AAAA Lua answers.
Core operational credentials remain database-backed platform settings. Configure its API URL as http://pdns-auth:8081, server ID as localhost, and use the same API key through the admin settings API or UI.
The local defaults publish authoritative DNS on port 5353, the API on loopback port 8089, and Poweradmin on loopback port 8084. Production deployments should normally publish authoritative DNS on TCP/UDP 53.
Lua records and edns-subnet-processing=yes are enabled. The MMDB updater uses DB-IP City Lite through jsDelivr by default; set CDNLITE_MMDB_DOWNLOAD_URL and an expected SHA-256 when pinning an internal artifact.
infra/dnsgeo/geo/lua-bootstrap.yml contains only geoip-bootstrap.invalid, a reserved non-routable zone required to initialize the GeoIP backend. CDNLite does not seed customer/example zones or Lua records. Core owns every real zone and record through the PowerDNS API.
DNS Acceptance Tests
ci/dns_e2e.sh uses the normal root Compose topology and fails unless Core writes the expected raw apex Lua, CNAME, DNS-only, site-target, and shared Lua records. It also compares dig answer sets for the apex, site target, and proxy host; verifies edge health changes update the shared CDN record and managed apex Lua records without rewriting non-apex customer records; checks stale deletion; and proves failed writes are visible and recoverable.
The script inspects the running authoritative configuration for expand-alias=yes and a separate resolver. Disabling either requirement fails CI before the DNS assertions run.
Core records each write attempt and verified outcome in dns_sync_events, and stores the current per-zone result in dns_sync_state. A failed PATCH or read-back verification leaves the zone in failed state with its status code and error. Inspect or operate the integration with:
docker compose exec core php artisan cdn:powerdns:doctor
docker compose exec core php artisan cdn:powerdns:dry-run
docker compose exec core php artisan cdn:powerdns:force-sync
curl -fsS http://localhost:8080/cdn-healthdoctor reports per-zone SOA validity, including missing, duplicate, invalid authority, stale serial, and timing problems. dry-run includes the SOA repair rrset it would apply without writing PowerDNS.
PostgreSQL Replication
The primary image configures WAL streaming, TLS, SCRAM authentication, a dedicated replicator role, and client certificates. The optional infra/dnsgeo/docker/postgres-replica image bootstraps a read-only standby with pg_basebackup. It is not a default root Compose service; production operators can place replicas according to their topology while local and CI remain deterministic.
Set PDNS_REPLICATION_PASSWORD for fresh initialization even when no replica is started. SQL initialization creates schemas and service roles only; it creates no customer, base, sample, or seed zones.
Validation
docker compose ps
curl -fsS -H "X-API-Key: $PDNS_API_KEY" \
http://127.0.0.1:8089/api/v1/servers/localhost
dig @127.0.0.1 -p 5353 example.net SOA
./ci/powerdns_dns_checks.shThe CI check creates an isolated zone through the real API, writes an rrset, resolves it through the authoritative listener with dig, verifies bad-key rejection, and removes the zone.
The reconciler writes each changed RRset independently. A rejected RRset is reported in DNS sync health and events, while unrelated RRsets and zones continue reconciling. Exact duplicate customer records are rejected by the API with dns_record_duplicate; different values for a shared name and type are combined into a multi-value RRset.
Customer record types are limited to A, AAAA, CNAME, TXT, MX, CAA, NS, and SRV. Names may be entered as @, relative labels, or an FQDN inside the customer zone; stored names are canonicalized to @ or a relative name. TTLs must be 60-86400 seconds and MX priorities must be 0-65535.
Production Stress Qualification
ci/stress-dns.sh is the destructive production-scale proof. It uses the root Compose topology and defaults to 10,000 customer zones with 1,000 records each. The runner verifies dataset counts and query indexes, performs a full sync, changes one edge IP, and proves no customer zone serial changed from that edge event. It also exercises repeated health transitions, concurrent customer DNS updates, advisory-lock behavior, stale/duplicate desired-state checks, /cdn-health responsiveness, and final PowerDNS health.
Results are written to ci/reports/dns-stress-report.json and ci/reports/dns-stress-report.md. The run destroys Core and PowerDNS data and must never target a shared or production database.
Follow DNS Stress Testing for prerequisites, reduced and full qualification commands, configuration, pass criteria, and troubleshooting. Operators can create and edit records before registrar delegation is complete. Those records remain stored but are not published until the domain's expected nameservers are verified. The nameserver-scheduler rechecks every domain daily by default and automatically withdraws records and edge configuration when delegation no longer matches. Set CDNLITE_NAMESERVER_CHECK_INTERVAL_SECONDS to change that interval.
Adding another proxied A/AAAA target at a hostname that already has a proxied record creates another origin row for the same host. CDNLite keeps the public LUA or CNAME stable and balances between the enabled origin targets.
At the zone apex, the PowerDNS LUA used for proxying may coexist with normal apex records such as MX, TXT, and CAA. A real CNAME remains exclusive.
