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 — configured with a single JSON file.

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


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.internal support
  • 🚫 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

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.0.tar.gz (41.0 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.0-py3-none-any.whl (42.9 kB view details)

Uploaded Python 3

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

Hashes for nanodns-1.0.0.tar.gz
Algorithm Hash digest
SHA256 782a2788b8c938237276315bd789acf89b6a69bb1174bf2860765fb3f4349dbf
MD5 ae85256f10f6da87bbd8ac7359c3ab40
BLAKE2b-256 7dd893e4f298cf3312d6e79ae1bf7cf7b626053eecaa1e4ab56fcaa185b5cc53

See more details on using hashes here.

Provenance

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

Hashes for nanodns-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1c1edb27225fe6d489badec4cb3d4c865b02ac2abd4efbb64ba4dcb6aceeb7cf
MD5 b5102024f37f572c9238cfbe9a8ae3b7
BLAKE2b-256 272f9624b8183c5cecf1076bd0ed060612d172fb1d66634c457556f76e2b868c

See more details on using hashes here.

Provenance

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