Start here

DNS in one picture

Your device asks where a website lives; DNS answers with its address ๐Ÿ“ฑ Your phone ๐Ÿ“– DNS (TelltaleDNS) "Where is youtube.com?" "At 142.250.72.14" This happens before every website, app update, or video.

DNS is your network's phone book. Because every connection starts with a lookup, the DNS server is the one place that can see โ€” and decide โ€” what your devices talk to.

  • Block ads and trackers for every device, even smart TVs.
  • Encrypt lookups so your ISP can't read them.
  • Spot the gadget that phones home at 3 a.m.

Step 1

Pick where it runs

Every path ends the same way: your router hands out TelltaleDNS as the DNS server, and every device uses it, with its own address visible. Pre-1.0: installs follow builds from main (edge).

Raspberry Pi with Docker Compose ยท the recommended home setup, about 10 minutes

Devices ask the Pi; the Pi answers from its cache or asks an encrypted upstream Your devices ๐Ÿ“ฑ ๐Ÿ’ป ๐Ÿ“บ get the Pi as DNS from the router (DHCP) port 53 ๐Ÿ“ Raspberry Pi TelltaleDNS container host network ยท read-only blocklists ยท cache ยท query log UI on :8053 DNS over TLS Cloudflare / Quad9 only for names not cached
Host networking is what lets TelltaleDNS see each device's own address (and MAC), so per-device statistics and rules work.
  1. Prepare the Pi. Raspberry Pi OS (64-bit recommended) on a Pi 3, 4, 5, or Zero 2 W, with a fixed address (set a DHCP reservation in your router), and Docker:
    curl -fsSL https://get.docker.com | sh
    sudo usermod -aG docker $USER    # log out and back in
  2. Free port 53. If Pi-hole or another DNS server runs on the Pi, stop it. Check: sudo ss -ulpn 'sport = :53' should print nothing.
  3. Download the bundle and start it.
    mkdir ~/telltale && cd ~/telltale
    curl -fsSLO https://raw.githubusercontent.com/paimonsoror/telltaledns/main/deploy/compose/compose.yaml
    curl -fsSLO https://raw.githubusercontent.com/paimonsoror/telltaledns/main/deploy/compose/telltale.toml
    docker compose up -d
    docker compose ps        # STATUS shows "healthy" within a minute
  4. Test it from another computer (replace the address with your Pi's):
    dig @192.168.1.2 example.com          # an address: it resolves
    dig @192.168.1.2 doubleclick.net      # 0.0.0.0 or NXDOMAIN: the blocklist works
    On Windows: nslookup example.com 192.168.1.2.
  5. Create the admin account. Print the one-time setup token and open the UI at http://<pi>:8053/:
    docker compose exec telltale telltale auth setup-token

Updating: docker compose pull && docker compose up -d. Editing the config: change telltale.toml, then docker compose kill -s HUP applies it without dropping a query. On an SD card, the starter config keeps the query log for 7 days; a USB SSD is better for longer history.

Raspberry Pi or Linux, native (systemd)

No Docker: a single static binary as a sandboxed systemd service, on any systemd distribution (x86-64, arm64, or 32-bit ARM). The installer checks the release's signature and checksum before installing anything, so read it, then run it:

curl -fsSLO https://raw.githubusercontent.com/paimonsoror/telltaledns/main/deploy/systemd/install.sh
less install.sh
sudo sh install.sh                 # the latest release; --edge: builds from main
sudo -u telltale telltale auth setup-token -c /etc/telltale/telltale.toml
  • It asks first: press Enter for the recommended settings, or walk me through the options to pick upstreams (encrypted presets or your own server), starting blocklists (or none), the listen address, the first admin (a setup token, or a password now), and the query log's retention and privacy. Every step shows its default; Enter keeps it. Piped or automated runs (and --yes) ask nothing.
  • It creates a telltale user, writes the config to /etc/telltale/telltale.toml (never overwriting yours), and keeps data in /var/lib/telltale.
  • If systemd-resolved holds port 53 (Ubuntu, some Debian setups), it offers to turn the stub listener off (--disable-resolved-stub does it without asking).
  • Updates: sudo telltale self-update --restart (add --channel edge if you installed with --edge). The previous binary is kept as telltale.old.
  • Logs: journalctl -u telltale -f. Reload after editing the config: sudo systemctl reload telltale.
  • Uninstall: sudo sh install.sh --uninstall. It removes the service and binary, offers to give the machine its own resolver back (systemd-resolved, as it was before), and keeps your config and data unless you ask it to delete them (--purge).

Docker

docker run -d --name telltale --read-only --cap-drop ALL \
  -p 53:53/udp -p 53:53/tcp -p 8053:8053 -p 9153:9153 \
  -v "$PWD/telltale.toml:/etc/telltale/telltale.toml:ro" \
  -v telltale-data:/var/lib/telltale \
  ghcr.io/paimonsoror/telltale:latest
docker exec telltale telltale auth setup-token

amd64, arm64, and arm/v7; about 13 MiB compressed, web UI included; runs as a non-root user. Published ports can hide each device's own address behind Docker's (the UI warns if they do): on a home network, --network host (or the Pi Compose bundle) makes sure each device shows up as itself. A starter config is in step 2.

Kubernetes with Helm

Devices reach a LoadBalancer IP; externalTrafficPolicy Local keeps their addresses on the way to the pod ๐Ÿ“ฑ ๐Ÿ’ป Devices 53 ยท 853 ยท 443 LoadBalancer IP MetalLB / Cilium / kube-vip externalTrafficPolicy: Local client IP kept โ˜ธ๏ธ TelltaleDNS pod StatefulSet + volume UI via Ingress ServiceMonitor ยท alerts
helm install telltale oci://ghcr.io/paimonsoror/charts/telltale \
  -n telltale --create-namespace \
  --set service.dns.annotations."metallb\.universe\.tf/loadBalancerIPs"=192.168.1.53
kubectl -n telltale get svc telltale-dns     # EXTERNAL-IP: your DNS server address
kubectl -n telltale exec telltale-0 -- /usr/local/bin/telltale auth setup-token \
  -c /etc/telltale/00-chart.toml -c /etc/telltale/10-values.toml
  • Every option: helm show values oci://ghcr.io/paimonsoror/charts/telltale > values.yaml prints the chart's commented values file; the Helm values reference lists each key with its default and meaning. Keep what you change in your own file and pass it with -f.
  • Chart versions: without --version, Helm installs the newest stable release (chart and image versions match). Builds from main publish X.Y.Z-edge.N charts with the :edge image: add --devel for the newest, or pin one with --version.
  • Keep externalTrafficPolicy: Local (the default): with Cluster, every query looks like it came from a node, and TelltaleDNS shows a "client IPs appear masked" banner.
  • Your settings go in config: (TOML, the same keys as the configuration reference); secrets come from existing Secrets.
  • Encrypted DNS for phones: encrypted.dot.enabled / encrypted.doh.enabled with a cert-manager certificate.
  • Monitoring: serviceMonitor.enabled, prometheusRule.enabled, grafanaDashboard.enabled.
  • More pods: mode: scaled adds resolver pods beside the controller (below).

From source

git clone https://github.com/paimonsoror/telltaledns
cd telltaledns
cargo build --release
sudo ./target/release/telltale run -c telltale.toml   # a config from step 2
dig @127.0.0.1 example.com

Needs a recent Rust toolchain (the repository pins it). The first start creates a one-time setup token; telltale auth setup-token prints it. Open the web UI on port 8053 and use it to create your admin.

Check what you run. Release binaries come with a signed SHA256SUMS (the installer and telltale self-update check it for you), and images are signed with cosign through GitHub's keyless signing:

cosign verify ghcr.io/paimonsoror/telltale:<version> \
  --certificate-identity-regexp '^https://github\.com/paimonsoror/telltaledns/\.github/workflows/image\.yml@' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Each binary carries its dependency list (cargo audit bin telltale reads it), every release has a CycloneDX SBOM per binary, and two builds of the same commit are byte for byte identical. How to rebuild and compare.

Step 2

Configure (optional)

The bundles and the installer come with a working configuration. This is a minimal one with encrypted upstreams, saved as telltale.toml:

[[upstream]]
name = "cloudflare"
url = "tls://1.1.1.1"
tls_server_name = "cloudflare-dns.com"

[[upstream]]
name = "quad9"
url = "https://dns.quad9.net/dns-query"
bootstrap = ["9.9.9.9"]

[[upstream_group]]
name = "default"
members = ["cloudflare", "quad9"]
strategy = "fastest"

Upstreams, blocklists, groups, schedules, and alerts can be added or changed later on their pages in the web UI, with a preview before anything is applied. Your config file is never rewritten. Every key is in the configuration reference.

Prefer a config file only, or Git? Choose how you manage settings.

Step 3

Your router hands out TelltaleDNS as the DNS server to every device ๐Ÿ“ถ Router (DHCP) ๐Ÿ“ฑ ๐Ÿ’ป๐Ÿ“บ ๐ŸŽฎ๐Ÿ“ท ๐Ÿ”Š "Use 192.168.1.53 for DNS" โ†’ every device now asks TelltaleDNS

Point your network at it

  1. In your router's DHCP (LAN) settings, set the DNS server to TelltaleDNS's address (the Pi, or the LoadBalancer IP). If there's a second field, use a second TelltaleDNS, or leave it empty: a public resolver there lets devices skip your blocking.
  2. Renew leases: reconnect Wi-Fi, or wait for leases to expire (often a day).
  3. Open the dashboard: devices appear under Clients with their own addresses. Give them names and groups there.

Don't point the router's own upstream DNS at TelltaleDNS while devices use the router: every query would then come from the router, and TelltaleDNS would say client IPs appear masked.

Grow

A Pi and Kubernetes, one cluster

A Pi and Kubernetes pods share one configuration; either side can be managed ๐Ÿ“ Pi (Compose or native) answers on its own if the link drops second DNS server in DHCP โ˜ธ๏ธ Kubernetes (Helm) primary DNS server UI, history, alerts mTLS ยท signed config one management plane
Give DHCP both DNS servers: each answers on its own, from the same replicated configuration.

telltale cluster init --name home --advertise https://192.168.3.2:8443 on the first node, telltale cluster token create there, then telltale cluster join tt_join_โ€ฆ on the other. Only the first node's port 8443 must be reachable; the others connect out. The first node's configuration, blocklists, users, and tokens then reach every node within seconds (signed, and only what changed), and each node keeps answering from its last copy if the link drops.

  • One management plane. Any node's UI shows the whole cluster's dashboard, query log, and live view, and changes made there go to the primary. Sign in to any node with the same account.
  • Failover. Promote another node by hand (telltale cluster promote), or let the cluster elect one (telltale cluster set-failover auto) once it has a third voter, such as a tiny witness (telltale cluster witness).
  • Node-local settings. Each node keeps its own listeners, retention, and overrides; everything else is shared.
  • More pods in Kubernetes. mode: scaled adds resolver pods beside the controller (resolvers.replicas). They join on their own, share the DNS Service, ship their query log and dashboard counts to the controller, and keep answering for a few seconds while the Service drops them, so rollouts don't lose lookups. For one address for DNS, the UI, and the cluster port, set service.dns.alsoServe.
  • See it. The Cluster page draws the cluster live: sites, the primary, pods grouped by Kubernetes node, link round trips, and each node's share of the queries.
  • Git as the source. Optionally, the shared configuration can come from a Git repository or a GitOps tool (config_source = "gitops").
  • Maintenance. Rolling upgrades work across one version step (N and Nโˆ’1), and telltale cluster rotate-ca replaces the cluster's certificate authority without downtime.

Coming from Pi-hole or Technitium?

Bring your setup with you

  1. In Pi-hole, open Settings โ†’ Teleporter and click Export (v6 gives a .zip, v5 a .tar.gz).
  2. Run telltale import pihole pi-hole_teleporter.zip -o pihole.toml. It carries over your upstreams, conditional forwarding, local DNS and CNAME records, adlists, allowed and denied domains (exact and regex), groups, clients, and DHCP reservations as named devices.
  3. Read the notes at the top of pihole.toml: they list everything that has no equivalent (for example clients matched by host name, or the DHCP server). Then add the file to your configuration.

Query history and passwords aren't imported. Keep Pi-hole running until TelltaleDNS answers the way you expect, then point your router at TelltaleDNS.

From Technitium: create an API token in Technitium (Administration โ†’ Sessions), then run TECHNITIUM_TOKEN=โ€ฆ telltale import technitium http://your-server:5380 -o technitium.toml. Forwarders, zones and records, forwarder zones, block lists, Advanced Blocking groups, and DHCP reservations come across. The notes at the top of the file list the rest.

If something's off

Troubleshooting

"address already in use" on port 53
Another DNS server runs there: Pi-hole, dnsmasq, or systemd-resolved's stub (sudo ss -ulpn 'sport = :53' names it). Stop it, or for systemd-resolved add DNSStubListener=no under [Resolve] in /etc/systemd/resolved.conf.d/telltale.conf and restart it.
A site is broken
Find it in the Query log and press Why?: it names the list and line that blocked it. Then allow it with a quick rule, or add an allow line (@@||name^) to a list.
Every query shows the same address
Something in between rewrites addresses: a router forwarding DNS, Docker without host networking, or Kubernetes with externalTrafficPolicy: Cluster. The UI's banner says which address; the client IP guide has the fixes.
Lost the setup token
telltale auth setup-token prints it again until the first admin exists.

Full running guide โ†’