A lightweight, JSON-configurable DNS server for internal networks
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4501cf111883cd7e7edc29fbc5b4a28e552ba794ad590d72473ee890cab922b0
|
|
| MD5 |
a85bbc6df1af1648162d17834fc04f11
|
|
| BLAKE2b-256 |
dc08e082207962764573893dbe9828a5d1f76b0c7f6575be02313877b249a9b0
|
Provenance
The following attestation bundles were made for nanodns-1.0.4.tar.gz:
Publisher:
release.yml on iyuangang/nanodns
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nanodns-1.0.4.tar.gz -
Subject digest:
4501cf111883cd7e7edc29fbc5b4a28e552ba794ad590d72473ee890cab922b0 - Sigstore transparency entry: 1094058631
- Sigstore integration time:
-
Permalink:
iyuangang/nanodns@0f141ee290c557223624013f826a037ed8692bc0 -
Branch / Tag:
refs/tags/v1.0.4 - Owner: https://github.com/iyuangang
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0f141ee290c557223624013f826a037ed8692bc0 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5e3dc2d731ca2896031fe07212eed52e1902619269c7aeb38681d8a6a84ed41
|
|
| MD5 |
462b963f2a84f22bac620f9cb342b581
|
|
| BLAKE2b-256 |
04e0ce86de8f663f0ec465d4b28f2d13aff837908a935086179240ad79828319
|
Provenance
The following attestation bundles were made for nanodns-1.0.4-py3-none-any.whl:
Publisher:
release.yml on iyuangang/nanodns
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nanodns-1.0.4-py3-none-any.whl -
Subject digest:
e5e3dc2d731ca2896031fe07212eed52e1902619269c7aeb38681d8a6a84ed41 - Sigstore transparency entry: 1094058681
- Sigstore integration time:
-
Permalink:
iyuangang/nanodns@0f141ee290c557223624013f826a037ed8692bc0 -
Branch / Tag:
refs/tags/v1.0.4 - Owner: https://github.com/iyuangang
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0f141ee290c557223624013f826a037ed8692bc0 -
Trigger Event:
push
-
Statement type: