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:
- Edit config on any node.
curl -X POST http://localhost:9053/reload- NanoDNS bumps the version, applies in memory, pushes to all peers in < 1 s.
- 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_hostto an internal interface only. Keepmgmt_portoff 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
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)
| File | Size | Uploaded | |
|---|---|---|---|
| nanodns-1.0.4.tar.gz | 42.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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