Skip to main content

A lightweight, JSON-configurable DNS server for internal networks

Project description

NanoDNS

A lightweight, zero-dependency DNS server for internal networks — one JSON file, no moving parts, with built-in multi-node HA.

PyPI version Python License: MIT CI codecov Docker Pulls GHCR OCI Signed


Features

🚀 Zero dependencies Pure Python standard library — nothing to install beyond the package itself
📝 Single JSON config Human-readable, validated with nanodns check, hot-reloaded every 5 s
🌐 Full DNS record support A, AAAA, CNAME, MX, TXT, PTR, NS, SOA
🃏 Wildcard records *.app.internal catches all single-level subdomains
🚫 Domain blocking Instant NXDOMAIN via rewrite rules — no upstream query, sub-ms response
🔄 Upstream forwarding Unknown names forwarded to public DNS with automatic per-server failover
💾 LRU cache Configurable size and TTL cap; flushed automatically on config change
♻️ Hot reload File-mtime polling every 5 s; bad JSON is rejected and the old config kept
🏥 HTTP management API /health, /ready, /metrics, /cluster, /reload, /sync
🔗 Built-in HA sync Versioned push + pull replication; anti-rollback; automatic node catch-up
Async Single-process, asyncio-based — handles concurrent queries efficiently
🐳 Docker Distroless OCI image, linux/amd64 + linux/arm64, cosign-signed

Installation

# PyPI  — CLI command is `nanodns`
pip install nanodns

# Docker
docker pull ghcr.io/iyuangang/nanodns:latest

Requires Python 3.10+.


Quick Start

# Generate a starter config
nanodns init

# Check it
nanodns check nanodns.json

# Run on a high port (no root needed)
nanodns start --config nanodns.json --port 5353

# Verify
dig @127.0.0.1 -p 5353 web.internal.lan A   # Linux / macOS
nslookup web.internal.lan 127.0.0.1          # Windows

Port 53 in production:

sudo nanodns start --config nanodns.json

Configuration

{
  "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": true,
    "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, "comment": "matches *.app.internal.lan" }
  ],
  "rewrites": [
    { "match": "*.ads.example.com", "action": "nxdomain" }
  ]
}

→ Full field reference in USAGE.md.


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 a config file and print a summary
nanodns --version

Record Types

Type value 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 types also accept: ttl (default 300 s), wildcard (bool), comment (string, ignored at runtime).


HTTP Management API

Enable by setting mgmt_port to a non-zero value (recommended: 9053).

Endpoint Method Description
/health GET Liveness probe — 503 when unavailable (use with Keepalived / HAProxy)
/ready GET Readiness probe — 503 until config is loaded
/metrics GET Cache stats, record count, config version, uptime
/cluster GET This node plus every peer: version and reachability
/config/raw GET Raw config JSON — fetched by peers during catch-up
/reload POST Reload from disk, bump version, push to all peers
/sync POST Accept a versioned config push from a peer (anti-rollback enforced)
curl http://localhost:9053/health
curl -X POST http://localhost:9053/reload | python3 -m json.tool
curl http://localhost:9053/cluster | python3 -m json.tool

Security: bind mgmt_host to an internal interface and keep mgmt_port off the public internet.


High Availability

NanoDNS solves HA at two independent layers without any external dependencies.

Traffic availability — network layer

DNS natively supports multiple nameservers. Pick the option that fits your infrastructure:

Option How Failover time
Multiple nameservers in /etc/resolv.conf Client retries next server on timeout ~1–3 s
Keepalived floating VIP VRRP detects unhealthy node via /health, moves IP ~4 s
HAProxy UDP load balancer Health-checks /health; removes unhealthy backends ~4 s
Kubernetes Service Readiness probe on /ready; pod removed from endpoints ~5 s

Config consistency — application layer

NanoDNS keeps every node's config in sync using a versioned push/pull protocol.

Push (online nodes): any POST /reload bumps config_version, writes the new version to disk, applies it in memory, then calls POST /sync on every peer. A peer rejects a push whose version is lower than its own (409 rejected_stale), preventing accidental rollback.

Pull (catch-up after restart): 10 seconds after startup, the node queries every peer's /health to collect versions, pulls the full config from the highest peer via GET /config/raw, writes it to disk, and applies it — no operator action needed. This reconciliation repeats every 30 seconds.

Scenario Convergence
Online node receives push < 1 s
Restarted node catches up 10–40 s
Periodic reconciliation ≤ 30 s

3-node cluster example

Each node's peers lists the management addresses of the other nodes. config_version is managed automatically — never edit it manually.

{
  "server": {
    "port": 53,
    "mgmt_port": 9053,
    "peers": ["10.0.0.12:9053", "10.0.0.13:9053"],
    "config_version": 1
  }
}
Node peers
ns1 10.0.0.11 ["10.0.0.12:9053", "10.0.0.13:9053"]
ns2 10.0.0.12 ["10.0.0.11:9053", "10.0.0.13:9053"]
ns3 10.0.0.13 ["10.0.0.11:9053", "10.0.0.12:9053"]

Updating records (zero downtime)

# Edit config on any node, then:
curl -s -X POST http://localhost:9053/reload | python3 -m json.tool
# {
#   "status": "reloaded",
#   "version": 5,
#   "peers": {
#     "10.0.0.12:9053": {"status": "applied", "version": 5},
#     "10.0.0.13:9053": {"status": "applied", "version": 5}
#   }
# }

See USAGE.md → High Availability for complete deployment guides: Keepalived, HAProxy, Kubernetes, and Docker Compose.


Docker

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
    cap_add: [NET_BIND_SERVICE]

3-node HA cluster

The project ships a ready-to-use docker-compose.yml:

docker compose up -d

Image reference

Tag Description
latest Latest stable release
1.2.3 Pinned to an exact version
sha-a1b2c3 Pinned to a specific commit

Platforms: linux/amd64 · linux/arm64

Built on Chainguard distroless Python. Verify the 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

Deployment

Linux — systemd

# /etc/systemd/system/nanodns.service
[Unit]
Description=NanoDNS Server
After=network.target

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

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

Windows — NSSM service

# Run as Administrator
nssm install NanoDNS "C:\Python\Scripts\nanodns.exe"
nssm set NanoDNS AppParameters "start --config C:\dns\nanodns.json"
nssm start NanoDNS

CI/CD

Commit prefix conventions

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

Releasing a new version

# 1. Bump version in pyproject.toml and nanodns/__init__.py
# 2. Commit, tag, push
git add pyproject.toml nanodns/__init__.py
git commit -m "chore: bump version to 0.2.0"
git tag v0.2.0 && git push origin main --tags
# PyPI (nanodns) and Docker publish automatically

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.1.tar.gz (43.1 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.1-py3-none-any.whl (46.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nanodns-1.0.1.tar.gz
  • Upload date:
  • Size: 43.1 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.1.tar.gz
Algorithm Hash digest
SHA256 0d4b5b642463998d555c8c576e14d94e756e2000241a4850d75b82f726f5f796
MD5 455a271fb0264d9a1b1e3cc4eb4c01a8
BLAKE2b-256 d1c523225d1b03002e1d46230f30c5325f97439c93813cd44e941891fe1c8241

See more details on using hashes here.

Provenance

The following attestation bundles were made for nanodns-1.0.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: nanodns-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 46.0 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9e13ed2b67536048bbb6eb0329d37af27fbbbf63922de2b8aee3550f4565034d
MD5 1986e45562e880d436588b280cea1285
BLAKE2b-256 6d04016e4bb159479bbede22d6fd416b0856df67d04ef2cb680c62484b3ff01a

See more details on using hashes here.

Provenance

The following attestation bundles were made for nanodns-1.0.1-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