Skip to main content

A lightweight, JSON-configurable DNS server for internal networks

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nanodns-1.0.4.tar.gz (42.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nanodns-1.0.4-py3-none-any.whl (45.8 kB view details)

Uploaded Python 3

File details

Details for the file nanodns-1.0.4.tar.gz.

File metadata

  • Download URL: nanodns-1.0.4.tar.gz
  • Upload date:
  • Size: 42.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for nanodns-1.0.4.tar.gz
Algorithm Hash digest
SHA256 4501cf111883cd7e7edc29fbc5b4a28e552ba794ad590d72473ee890cab922b0
MD5 a85bbc6df1af1648162d17834fc04f11
BLAKE2b-256 dc08e082207962764573893dbe9828a5d1f76b0c7f6575be02313877b249a9b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for nanodns-1.0.4.tar.gz:

Publisher: release.yml on iyuangang/nanodns

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nanodns-1.0.4-py3-none-any.whl.

File metadata

  • Download URL: nanodns-1.0.4-py3-none-any.whl
  • Upload date:
  • Size: 45.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for nanodns-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 e5e3dc2d731ca2896031fe07212eed52e1902619269c7aeb38681d8a6a84ed41
MD5 462b963f2a84f22bac620f9cb342b581
BLAKE2b-256 04e0ce86de8f663f0ec465d4b28f2d13aff837908a935086179240ad79828319

See more details on using hashes here.

Provenance

The following attestation bundles were made for nanodns-1.0.4-py3-none-any.whl:

Publisher: release.yml on iyuangang/nanodns

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page