
How to manage DNS for multiple domains efficiently?
To manage DNS for multiple domains efficiently, start with an inventory of every domain, its registrar and its DNS provider, then consolidate them onto as few DNS providers as you sensibly can. Define your common records, such as email authentication, CAA and redirects, once as a template, apply it to every domain with a DNS-as-code tool like DNSControl or octoDNS, and protect parked domains with records that stop them being used for spoofing. Finally, add monitoring for expiry dates and unexpected changes, so nothing drifts or lapses without you noticing.
Managing one domain by clicking around a dashboard is fine. Managing twenty, or two hundred, the same way leads to inconsistent records, forgotten renewals, and domains that nobody remembers owning until they're used for phishing. This guide covers how to organise a domain portfolio, how to reuse configuration across domains, how to secure the ones you don't actively use, and how to automate changes and checks.
Start With an Inventory
You can't manage what you can't see. Build a single list of every domain your organisation owns, with at least these columns:
| Field | Why it matters |
|---|---|
| Domain | The name itself |
| Registrar | Where renewals and nameserver changes happen |
| Expiry date | Lapsed domains can be registered by anyone |
| DNS provider | Where records are actually edited |
| Purpose | Live site, email, redirect, defensive, retired |
| Owner | The team or person responsible |
| DNSSEC | Whether a DS record is published |
If you don't know where a domain's DNS is hosted, you can find out from its nameservers:
dig example.com NS +short
ada.ns.cloudflare.com.
bob.ns.cloudflare.com.
For a list of domains, a short loop gives you a quick picture of which providers you're using:
#!/usr/bin/env bash
# ns-inventory.sh - print the nameservers for each domain in domains.txt
while read -r domain; do
[ -z "$domain" ] && continue
ns=$(dig +short "$domain" NS | sort | tr '\n' ' ')
printf "%-28s %s\n" "$domain" "${ns:-NO NS FOUND}"
done < domains.txt
example.com ada.ns.cloudflare.com. bob.ns.cloudflare.com.
example.net ns-123.awsdns-15.com. ns-842.awsdns-41.net. ...
example.org dns1.registrar.example. dns2.registrar.example.
Registration data such as the registrar and expiry date is available through RDAP, the modern replacement for WHOIS. Most registries publish RDAP over HTTPS, and you can query it with curl:
curl -s https://rdap.verisign.com/com/v1/domain/example.com \
| jq -r '.events[] | select(.eventAction=="expiration") | .eventDate'
The RDAP server differs by TLD. The rdap.org service redirects to the right one, so https://rdap.org/domain/example.com works as a general starting point.
Consolidate Your DNS Providers
Domains often end up spread across several providers: the registrar's free DNS for some, a CDN's DNS for others, a cloud provider for a few more. Each extra provider means another login, another permission model, another API, and another place a record can hide.
Pick one primary provider, or two at most, and move domains onto it over time. When you choose, look for:
- A good API with scoped tokens, so automation doesn't need full account access.
- Role-based access and audit logs, so you can see who changed what.
- Support for every record type you use, including CAA, SRV, HTTPS and ALIAS or CNAME flattening at the apex.
- Bulk operations or good support in DNS-as-code tools.
- DNSSEC that's easy to enable per domain.
Consolidation doesn't mean moving registrations. You can keep domains at different registrars and still point them all at the same DNS provider.
Group Domains by Role
Most portfolios fall into a handful of categories, and each category needs a similar set of records. Grouping them makes templating easy:
- Primary domains: Your main website and email domain. Full record sets, careful change control.
- Product or regional sites: Separate websites, often on the same hosting platform.
- Redirect domains: Typos, old brand names, country variants that should forward to a primary site.
- Defensive or parked domains: Registered to stop others using them, with no website or email.
- Retired domains: No longer used, kept until they can safely lapse.
The trick is to decide the standard records for each group once, rather than designing each domain from scratch.
Lock Down Parked and Unused Domains
Domains that don't send email are still attractive to spammers, because without SPF and DMARC records, receiving servers have no way to tell that mail claiming to be from them is fake. Every domain that doesn't send email should publish:
@ IN MX 0 .
@ IN TXT "v=spf1 -all"
_dmarc IN TXT "v=DMARC1; p=reject;"
*._domainkey IN TXT "v=DKIM1; p="
- Null MX (
MX 0 .): Declares that the domain doesn't accept email, as defined in RFC 7505. - SPF
-all: No server is allowed to send mail for this domain. - DMARC
p=reject: Receivers should reject anything that fails authentication, which is everything. - Empty DKIM key: An empty
p=tag signals that any DKIM key under this domain is revoked. Some people skip this one; the first three do most of the work.
You should also remove any leftover A, AAAA and CNAME records that point at services you no longer use. A CNAME to a deleted cloud resource is an invitation for a subdomain takeover, where someone else claims that resource and serves content on your domain.
If a defensive domain should redirect to your main site, point it at a redirect service rather than leaving stale records around.
Use DNS as Code
The biggest efficiency gain comes from managing records in files rather than dashboards. With DNS as code:
- Every domain's records live in a Git repository.
- Changes go through pull requests, so someone reviews them.
- Shared records are defined once and reused.
- A preview shows exactly what will change before anything is applied.
- The repository doubles as a backup and an audit trail.
Several tools do this well. Terraform is the most general and is worth considering if you already use it for infrastructure. Two tools built specifically for DNS are DNSControl and octoDNS, and both are excellent for portfolios of many domains.
DNSControl
DNSControl, originally built at Stack Overflow, uses a JavaScript configuration file, dnsconfig.js. Because it's JavaScript, you can use variables, functions and loops to build templates.
First, define provider credentials in creds.json:
{
"cloudflare": {
"TYPE": "CLOUDFLAREAPI",
"apitoken": "$CLOUDFLARE_API_TOKEN"
},
"none": {
"TYPE": "NONE"
}
}
The $CLOUDFLARE_API_TOKEN value is read from the environment, so the token itself never goes into the repository.
Then describe your domains in dnsconfig.js:
var REG_NONE = NewRegistrar("none");
var DSP_CF = NewDnsProvider("cloudflare");
// Records every domain should have
var COMMON = [
CAA("@", "issue", "letsencrypt.org"),
CAA("@", "iodef", "mailto:security@example.com"),
];
// Records for domains that never send email
var NO_EMAIL = [
MX("@", 0, "."),
TXT("@", "v=spf1 -all"),
TXT("_dmarc", "v=DMARC1; p=reject;"),
];
// Primary domain
D("example.com", REG_NONE, DnsProvider(DSP_CF),
COMMON,
A("@", "192.0.2.10"),
AAAA("@", "2001:db8::10"),
CNAME("www", "example.com."),
MX("@", 10, "mx1.mail.example.net."),
MX("@", 20, "mx2.mail.example.net."),
TXT("@", "v=spf1 include:_spf.mail.example.net -all"),
TXT("_dmarc", "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"),
END);
// Defensive domains share exactly the same records
var PARKED = ["example.net", "example.org", "example-shop.com"];
PARKED.forEach(function (name) {
D(name, REG_NONE, DnsProvider(DSP_CF),
COMMON,
NO_EMAIL,
A("@", "192.0.2.80"), // redirect server
CNAME("www", name + "."),
END);
});
Using NewRegistrar("none") means DNSControl only manages records and leaves nameserver delegation alone, which is the safest way to start.
Preview the changes, then apply them:
dnscontrol preview
******************** Domain: example.net
1 correction (cloudflare)
#1: + CREATE example.net MX 0 . ttl=300
Done. 1 corrections.
dnscontrol push
Adding a new defensive domain is now a one-line change to the PARKED list, reviewed in a pull request like any other code.
DNSControl can also import what's already there. dnscontrol get-zones reads existing zones from a provider and prints them in DNSControl format, so you don't have to type out records by hand when you first adopt it.
octoDNS
octoDNS, created at GitHub, takes a different approach. Records live in YAML files, one per zone, and a configuration file says which source files sync to which providers.
config/production.yaml:
providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: ./zones
cloudflare:
class: octodns_cloudflare.CloudflareProvider
token: env/CLOUDFLARE_API_TOKEN
zones:
example.com.:
sources:
- config
targets:
- cloudflare
zones/example.com.yaml:
---
'':
- type: A
values:
- 192.0.2.10
- type: MX
values:
- exchange: mx1.mail.example.net.
preference: 10
- exchange: mx2.mail.example.net.
preference: 20
- type: TXT
value: v=spf1 include:_spf.mail.example.net -all
www:
type: CNAME
value: example.com.
_dmarc:
type: TXT
value: v=DMARC1\; p=reject\; rua=mailto:dmarc@example.com
Semicolons in TXT values must be escaped with a backslash in octoDNS YAML, as shown above.
Run a dry run, then apply:
octodns-sync --config-file=config/production.yaml
octodns-sync --config-file=config/production.yaml --doit
octoDNS is a good fit if you want to push the same zone to two providers at once for redundancy, because a zone can list multiple targets.
Choosing Between Them
| DNSControl | octoDNS | Terraform | |
|---|---|---|---|
| Config language | JavaScript | YAML | HCL |
| Templates and loops | Native JavaScript | Via YAML anchors or generation | Modules and for_each |
| State file | None, reads live DNS | None, reads live DNS | Required |
| Multi-provider sync | Yes | Yes | Yes, with more setup |
| Best for | Large portfolios with shared patterns | Teams that like plain data files | Teams already using Terraform |
Not needing a state file is a real advantage for DNS. Both DNSControl and octoDNS compare your files with the live zone every time, so there's nothing to get out of sync.
Run Changes Through CI
Once records are in Git, add a pipeline so changes are previewed on every pull request and applied only when merged. A GitHub Actions workflow for DNSControl might look like this:
name: dns
on:
pull_request:
push:
branches: [main]
jobs:
dns:
runs-on: ubuntu-latest
container: ghcr.io/stackexchange/dnscontrol:latest
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Check config
run: dnscontrol check
- name: Preview
run: dnscontrol preview
- name: Push
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: dnscontrol push
Pin the container image to a specific version in production rather than latest, so an upgrade doesn't change behaviour unexpectedly. Use an API token scoped to DNS edit permissions on only the zones the pipeline manages.
Control Access
With many domains, it's tempting to share one admin login. Avoid it:
- Give each person their own account at the DNS provider and registrar, with two-factor authentication.
- Use scoped API tokens for automation, limited to DNS permissions and, where possible, specific zones.
- Restrict who can change nameservers at the registrar. That single setting controls every record.
- Turn on registrar lock (transfer lock) for every domain.
- Review access regularly and remove people who've left.
If everything goes through DNS as code, you can reduce dashboard write access to a small number of administrators for emergencies.
Monitor Expiry and Drift
Automation keeps records consistent, but you still need to know when something changes outside it or a renewal is coming.
- Expiry dates: Turn on auto-renew everywhere, then add a check that warns you 60 and 30 days before expiry anyway, using RDAP as shown earlier. Auto-renew fails quietly when a payment card expires.
- Drift detection: Run
dnscontrol previewor an octoDNS dry run on a schedule. If it shows changes nobody made in Git, someone edited the dashboard directly. - Delegation checks: Re-run the nameserver inventory script regularly to catch domains that were moved or whose delegation broke.
- Certificate expiry: Monitor TLS certificates on every live hostname.
A Quick Comparison of Approaches
| Approach | Works for | Main drawback |
|---|---|---|
| Dashboard editing | One to five domains | Inconsistent records, no review or history |
| Provider bulk tools | Small portfolios on one provider | Locked to that provider |
| Scripts against provider APIs | Specific repetitive tasks | You end up maintaining your own tool |
| DNS as code | Ten domains and up | Initial setup and learning time |
FAQ: Managing DNS for Multiple Domains
It usually makes management much easier, because you have one set of permissions, one API and one place to look. Larger organisations sometimes use two providers for redundancy, but they still manage both from the same tooling.
No. DNS hosting and registration are separate. Domains at any registrar can point to the same DNS provider by changing their nameservers, although consolidating registrars does simplify renewals.
At minimum, a null MX record, an SPF record of v=spf1 -all, and a DMARC record with p=reject. These tell receiving servers the domain never sends email, which stops spammers using it convincingly.
Both manage DNS as code across many providers without a state file. DNSControl uses a JavaScript configuration, which makes templates and loops easy, while octoDNS uses YAML zone files with a separate provider configuration.
Limit dashboard write access to a few administrators and give everyone else read-only access. Run a scheduled preview from your DNS-as-code tool so any change made outside Git shows up as drift.
Enable auto-renew on every domain and add a scheduled check that reads expiry dates from RDAP and alerts you well in advance. Payment failures are the usual reason auto-renew doesn't work.
Conclusion
Efficient DNS management across many domains is mostly about structure. Know what you own, keep it on as few providers as practical, group domains by role, and give each group a standard set of records. Defensive domains in particular deserve a few minutes of attention, because null MX, SPF and DMARC records stop them being used against your brand.
From there, DNS as code with DNSControl, octoDNS or Terraform turns repetitive dashboard work into reviewed, versioned changes that apply consistently everywhere. Add scoped access, drift checks and expiry monitoring, and a portfolio of hundreds of domains becomes no harder to look after than a handful.


