Skip to main content

NanoDNS

A lightweight internal DNS server — one JSON file, runs anywhere Python runs.

PyPI version Python Release Coverage License: MIT GHCR OCI Signed


pip install nanodns
nanodns start                      # listening on :53 in 2 seconds
{ "records": [
    { "name": "dev.local",  "type": "A", "value": "192.168.1.10" },
    { "name": "*.dev.local","type": "A", "value": "192.168.1.10", "wildcard": true }
]}
$ dig @127.0.0.1 api.dev.local +short
192.168.1.10                       # wildcard matched — no restart needed

Why NanoDNS?

You just need internal DNS for your homelab, a small team, or a dev environment. You don't need a 300 MB container with a web UI, a PostgreSQL backend, or a BIND config file that requires a PhD to edit.

NanoDNS Pi-hole CoreDNS dnsmasq
Zero pip dependencies ✅ ❌ ❌ ❌
Built-in multi-node HA sync ✅ ❌ ❌ ❌
Edit config with any text editor ✅ ⚠️ ⚠️ ⚠️
Hot-reload without restart ✅ ❌ ✅ ❌

NanoDNS is for people who want DNS to be boring — not a project in itself.


What people use it for

Homelab internal DNS Replace your router's dnsmasq with something you can actually version-control. Push a config change from your laptop, all nodes sync in under a second.

Dev environment *.svc.local → 127.0.0.1 with one wildcard record. No /etc/hosts hacks, no docker network DNS, no Consul. Edit JSON, it reloads.

Small private cloud (2–5 nodes) No Kubernetes, no CoreDNS Helm chart. Three VMs, three NanoDNS instances, peers set to each other's IPs. They stay in sync automatically.

Block unwanted domains Add rewrite rules that return NXDOMAIN instantly. No upstream query, sub-ms response, no separate blocklist service needed.


30-second quick start

# Install
pip install nanodns

# Create a config
nanodns init           # writes nanodns.json in the current directory

# Validate it
nanodns check nanodns.json

# Run on a non-privileged port to test
nanodns start --port 5353

# Query it
dig @127.0.0.1 -p 5353 web.internal A

Port 53 (requires root or CAP_NET_BIND_SERVICE):

sudo nanodns start --config /etc/nanodns/nanodns.json

Configuration in full

{
  "server": {
    "host": "0.0.0.0",
    "port": 53,
    "upstream": ["8.8.8.8", "1.1.1.1"],
    "cache_enabled": true,
    "cache_ttl": 300,
    "cache_size": 1000,
    "log_level": "INFO",
    "log_queries": false,
    "hot_reload": true,
    "mgmt_port": 9053,
    "peers": []
  },
  "zones": {
    "internal.lan": {
      "soa": {
        "mname": "ns1.internal.lan", "rname": "admin.internal.lan",
        "serial": 2024010101, "refresh": 3600, "retry": 900,
        "expire": 604800, "minimum": 300
      },
      "ns": ["ns1.internal.lan"]
    }
  },
  "records": [
    { "name": "web.internal.lan",   "type": "A",     "value": "192.168.1.100", "ttl": 300 },
    { "name": "db.internal.lan",    "type": "A",     "value": "192.168.1.101" },
    { "name": "api.internal.lan",   "type": "CNAME", "value": "web.internal.lan" },
    { "name": "internal.lan",       "type": "MX",    "value": "mail.internal.lan", "priority": 10 },
    { "name": "*.app.internal.lan", "type": "A",     "value": "192.168.1.200", "wildcard": true }
  ],
  "rewrites": [
    { "match": "ads.example.com",   "action": "nxdomain" },
    { "match": "*.tracker.net",     "action": "nxdomain" }
  ]
}

Every change to this file is detected within 5 seconds and applied live — no restart, no dropped queries, no operator action required.

→ Full reference: USAGE.md


Record types

Type value field Extra fields
A IPv4 address —
AAAA IPv6 address —
CNAME Target hostname —
MX Mail server hostname priority (int)
TXT Text string —
PTR Pointer hostname —
NS Nameserver hostname —

All records also accept: ttl (seconds, default 300), wildcard (bool), comment (string, ignored at runtime).


Multi-node HA

No Zookeeper. No Raft. No etcd. Just point each node at its peers.

{
  "server": {
    "mgmt_port": 9053,
    "peers": ["10.0.0.12:9053", "10.0.0.13:9053"]
  }
}

How sync works:

  1. Edit config on any node.
  2. curl -X POST http://localhost:9053/reload
  3. NanoDNS bumps the version, applies in memory, pushes to all peers in < 1 s.
  4. Nodes that were offline catch up automatically when they come back — they pull the latest config from the highest-versioned peer, with no operator action required.
$ curl -s http://localhost:9053/cluster | python3 -m json.tool
{
  "this": { "version": 5, "status": "healthy" },
  "peers": {
    "10.0.0.12:9053": { "version": 5, "status": "synced" },
    "10.0.0.13:9053": { "version": 5, "status": "synced" }
  }
}
Scenario Convergence time
Reload pushed to online peers < 1 s
Node reboots and catches up 10–40 s
Periodic background reconciliation ≤ 30 s

For traffic-level HA (floating VIP, HAProxy UDP, Kubernetes Service) see USAGE.md → High Availability.


HTTP management API

Enable with mgmt_port in config (recommended: 9053).

Endpoint Method Description
/health GET Liveness — 503 when unavailable
/ready GET Readiness — 503 until config loaded
/metrics GET Cache stats, query count, uptime, version
/cluster GET All nodes with version + reachability
/config/raw GET Raw config JSON (used by peer catch-up)
/reload POST Reload from disk, bump version, push to peers
/sync POST Accept a versioned config push from a peer

Bind mgmt_host to an internal interface only. Keep mgmt_port off the internet.


CLI

nanodns start   --config FILE [--host HOST] [--port PORT] [--log-level LEVEL] [--no-cache]
nanodns init    [OUTPUT]       Write an example config  (default: ./nanodns.json)
nanodns check   CONFIG         Validate config and print a summary
nanodns --version

Docker

# docker-compose.yml — single node
services:
  nanodns:
    image: ghcr.io/iyuangang/nanodns:latest
    restart: unless-stopped
    ports:
      - "53:53/udp"
      - "9053:9053/tcp"
    volumes:
      - ./nanodns.json:/etc/nanodns/nanodns.json:ro
    cap_add: [NET_BIND_SERVICE]

3-node cluster — the repo ships a ready-to-use docker-compose.yml:

git clone https://github.com/iyuangang/nanodns
cd nanodns && docker compose up -d

Image tags: latest · 1.2.3 (pinned) · sha-a1b2c3 (commit)
Platforms: linux/amd64 · linux/arm64
Base: Chainguard distroless Python — no shell, no package manager, minimal CVE surface.

Verify the image signature:

cosign verify \
  --certificate-identity-regexp="https://github.com/iyuangang/nanodns/.github/workflows/release.yml@refs/tags/.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/iyuangang/nanodns:latest

systemd (Linux production)

pip install nanodns
sudo cp /etc/systemd/system/nanodns.service << 'EOF'
[Unit]
Description=NanoDNS Server
After=network.target

[Service]
ExecStart=/usr/local/bin/nanodns start --config /etc/nanodns/nanodns.json
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now nanodns
sudo journalctl -u nanodns -f

Or use the RPM / DEB packages in the releases page — they install the unit file automatically.


Windows

pip install nanodns
nanodns start --config C:\dns\nanodns.json --port 5353   # test first

# Install as a service with NSSM (https://nssm.cc)
nssm install NanoDNS "C:\Python\Scripts\nanodns.exe"
nssm set NanoDNS AppParameters "start --config C:\dns\nanodns.json"
nssm start NanoDNS

Packages (RPM / DEB)

Pre-built packages are available on the releases page.

# RHEL / Rocky Linux / AlmaLinux / Fedora
sudo dnf install ./nanodns-*.noarch.rpm

# Debian / Ubuntu
sudo apt install ./nanodns_*.deb

Or build from source (no system packaging tools needed):

python3 packaging/build_rpm.py          # → dist/nanodns-*.rpm
python3 packaging/build_deb.py          # → dist/nanodns_*.deb
python3 packaging/build_packages.py --all   # both at once

Contributing

git clone https://github.com/iyuangang/nanodns
cd nanodns
pip install -e ".[dev]"
pytest                  # 202 tests, ~7 s
pytest --cov            # coverage ≥ 90 % enforced

Commit prefix → CI behaviour:

Prefix Tests Docker build PyPI publish
feat fix perf refactor ✅ ✅ on main 🏷️ on tag
test ci build ✅ ⏭️ skip ⏭️ skip
docs style chore ⏭️ skip ⏭️ skip ⏭️ skip

Bug reports, feature requests, and PRs are all welcome.


License

MIT

Metadata

Release files for nanodns 1.0.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nanodns 1.0.4
File Size Uploaded
nanodns-1.0.4.tar.gz 42.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nanodns 1.0.4
File Interpreter ABI Platform
nanodns-1.0.4-py3-none-any.whl Python 3 none any Details

Total release size: 88.7 kB

Release files / nanodns-1.0.4.tar.gz

Download URL nanodns-1.0.4.tar.gz
Size 42.8 kB
Tags Source
SHA-256 checksum
How to use checksums
4501cf111883cd7e7edc29fbc5b4a28e552ba794ad590d72473ee890cab922b0
BLAKE2b-256 checksum
How to use checksums
dc08e082207962764573893dbe9828a5d1f76b0c7f6575be02313877b249a9b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 13, 2026.

Transparency log

Release files / nanodns-1.0.4-py3-none-any.whl

Download URL nanodns-1.0.4-py3-none-any.whl
Size 45.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e5e3dc2d731ca2896031fe07212eed52e1902619269c7aeb38681d8a6a84ed41
BLAKE2b-256 checksum
How to use checksums
04e0ce86de8f663f0ec465d4b28f2d13aff837908a935086179240ad79828319
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.4 This release

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page