A lightweight, JSON-configurable DNS server for internal networks
Project description
NanoDNS
A lightweight, zero-dependency DNS server for internal networks — configured with a single JSON file.
Features
- 🚀 Zero dependencies — pure Python standard library only
- 📝 JSON config — human-readable, hot-reloadable configuration
- 🔄 Upstream forwarding — forwards unknown queries to public DNS
- 💾 LRU Cache — configurable in-memory response cache with TTL
- 🌐 Record types — A, AAAA, CNAME, MX, TXT, PTR, NS, SOA
- 🃏 Wildcard records —
*.example.internalsupport - 🚫 Rewrites / blocking — NXDOMAIN any domain instantly
- ♻️ Hot reload — config changes applied without restart
- ⚡ Async — built on Python asyncio
- 🐳 Docker — multi-platform OCI image (
amd64+arm64) - 🔏 Signed — cosign keyless signatures via Sigstore
Installation
pip
pip install nanodns
Docker
# GitHub Container Registry (recommended)
docker pull ghcr.io/iyuangang/nanodns:latest
# Docker Hub
docker pull iyuangang/nanodns:latest
Quick Start
pip
# 1. Generate an example config
nanodns init
# 2. Edit nanodns.json to your needs
# 3. Validate config
nanodns check nanodns.json
# 4. Start on a high port (no admin rights needed)
nanodns start --config nanodns.json --port 5353
# 5. Start on port 53 (requires root / Administrator)
sudo nanodns start --config nanodns.json
Docker
# Generate a config first
nanodns init nanodns.json
# Run with Docker
docker run -d \
--name nanodns \
-p 53:53/udp \
-v $(pwd)/nanodns.json:/etc/nanodns.json:ro \
--cap-add NET_BIND_SERVICE \
ghcr.io/iyuangang/nanodns:latest
# Run with Docker Compose
docker compose up -d
Verify it works
# Linux / macOS
dig @127.0.0.1 -p 5353 web.internal.lan A
# Windows (cmd)
nslookup web.internal.lan 127.0.0.1
# Windows (PowerShell)
Resolve-DnsName -Name web.internal.lan -Server 127.0.0.1 -Type A
Configuration
{
"server": {
"host": "0.0.0.0",
"port": 53,
"upstream": ["8.8.8.8", "1.1.1.1"],
"upstream_timeout": 3,
"upstream_port": 53,
"cache_enabled": true,
"cache_ttl": 300,
"cache_size": 1000,
"log_level": "INFO",
"log_queries": true,
"hot_reload": true
},
"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": "ns1.internal.lan", "type": "A", "value": "192.168.1.10", "ttl": 3600 },
{ "name": "web.internal.lan", "type": "A", "value": "192.168.1.100", "ttl": 300 },
{ "name": "db.internal.lan", "type": "A", "value": "192.168.1.101", "ttl": 300 },
{ "name": "api.internal.lan", "type": "CNAME", "value": "web.internal.lan" },
{ "name": "ipv6.internal.lan", "type": "AAAA", "value": "fd00::1", "ttl": 300 },
{ "name": "internal.lan", "type": "MX", "value": "mail.internal.lan", "priority": 10 },
{ "name": "internal.lan", "type": "TXT", "value": "v=spf1 ip4:192.168.1.0/24 ~all" },
{
"name": "app.internal.lan",
"type": "A",
"value": "192.168.1.200",
"wildcard": true,
"comment": "Matches *.app.internal.lan"
}
],
"rewrites": [
{ "match": "ads.doubleclick.net", "action": "nxdomain" },
{ "match": "*.tracker.example", "action": "nxdomain" }
]
}
See USAGE.md for the full configuration reference.
CLI Reference
nanodns start --config FILE [--host HOST] [--port PORT] [--log-level LEVEL] [--no-cache]
nanodns init [OUTPUT] Generate an example config file
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 records support: ttl (default 300), wildcard (bool), comment (string).
Docker
Images
| Registry | Image |
|---|---|
| GHCR | ghcr.io/iyuangang/nanodns |
| Docker Hub | iyuangang/nanodns |
Tags
| Tag | Description |
|---|---|
latest |
Latest stable release |
1.2.3 |
Exact version |
1.2 |
Minor version |
1 |
Major version |
sha-a1b2c3 |
Specific commit (main) |
Platforms
linux/amd64 · linux/arm64 (Raspberry Pi 4+, Apple Silicon via emulation)
docker-compose.yml
services:
nanodns:
image: ghcr.io/iyuangang/nanodns:latest
container_name: nanodns
restart: unless-stopped
ports:
- "53:53/udp"
volumes:
- ./nanodns.json:/etc/nanodns.json:ro
cap_add:
- NET_BIND_SERVICE
read_only: true
OCI Compliance & Supply Chain Security
Images are built on Chainguard distroless Python and follow the OCI Image Spec with standard annotations.
# Inspect OCI annotations
docker inspect ghcr.io/iyuangang/nanodns:latest \
--format '{{json .Config.Labels}}' | python3 -m json.tool
# Verify cosign keyless signature (Sigstore)
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
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now nanodns
Windows — NSSM
nssm install NanoDNS "C:\Python\Scripts\nanodns.exe"
nssm set NanoDNS AppParameters "start --config C:\dns\nanodns.json"
nssm start NanoDNS
High Port (No Root Required)
nanodns start --config nanodns.json --port 5353
# Redirect port 53 → 5353 with iptables (Linux)
sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 5353
CI / CD
Commit Convention
Commit prefix determines what runs in CI:
| Prefix | Tests | Docker build | PyPI release |
|---|---|---|---|
feat fix perf refactor |
✅ | ✅ on main | 🏷️ on tag |
test ci build |
✅ | ⏭️ skip | ⏭️ skip |
docs style chore |
⏭️ skip | ⏭️ skip | ⏭️ skip |
Workflows
| Workflow | Trigger | Description |
|---|---|---|
test.yml |
every push / PR | Unit + integration tests across 3 OS × 3 Python versions; uploads coverage to Codecov |
release.yml |
v* tag / main |
Full pipeline: test → build → summary → GitHub Release → PyPI → Docker |
Release a new version
# 1. Bump version in pyproject.toml
# 2. Commit and tag
git add pyproject.toml
git commit -m "chore: bump version to 0.2.0"
git tag v0.2.0
git push origin main --tags
PyPI and Docker Hub are published automatically on tag push.
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.0.tar.gz.
File metadata
- Download URL: nanodns-1.0.0.tar.gz
- Upload date:
- Size: 41.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
782a2788b8c938237276315bd789acf89b6a69bb1174bf2860765fb3f4349dbf
|
|
| MD5 |
ae85256f10f6da87bbd8ac7359c3ab40
|
|
| BLAKE2b-256 |
7dd893e4f298cf3312d6e79ae1bf7cf7b626053eecaa1e4ab56fcaa185b5cc53
|
Provenance
The following attestation bundles were made for nanodns-1.0.0.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.0.tar.gz -
Subject digest:
782a2788b8c938237276315bd789acf89b6a69bb1174bf2860765fb3f4349dbf - Sigstore transparency entry: 1038452514
- Sigstore integration time:
-
Permalink:
iyuangang/nanodns@2e53e1e90295552947f1f0731ef12bdce74fdd7c -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/iyuangang
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2e53e1e90295552947f1f0731ef12bdce74fdd7c -
Trigger Event:
push
-
Statement type:
File details
Details for the file nanodns-1.0.0-py3-none-any.whl.
File metadata
- Download URL: nanodns-1.0.0-py3-none-any.whl
- Upload date:
- Size: 42.9 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 |
1c1edb27225fe6d489badec4cb3d4c865b02ac2abd4efbb64ba4dcb6aceeb7cf
|
|
| MD5 |
b5102024f37f572c9238cfbe9a8ae3b7
|
|
| BLAKE2b-256 |
272f9624b8183c5cecf1076bd0ed060612d172fb1d66634c457556f76e2b868c
|
Provenance
The following attestation bundles were made for nanodns-1.0.0-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.0-py3-none-any.whl -
Subject digest:
1c1edb27225fe6d489badec4cb3d4c865b02ac2abd4efbb64ba4dcb6aceeb7cf - Sigstore transparency entry: 1038452615
- Sigstore integration time:
-
Permalink:
iyuangang/nanodns@2e53e1e90295552947f1f0731ef12bdce74fdd7c -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/iyuangang
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2e53e1e90295552947f1f0731ef12bdce74fdd7c -
Trigger Event:
push
-
Statement type: