Single-container docker-compose recipe for running numa in ODoH client mode. Ships with a starter numa.toml pointing at odoh-relay.numa.rs paired with Cloudflare's ODoH target — two independent operators with distinct eTLD+1s, so the default passes numa's same-operator check. Exposes :53 UDP+TCP for LAN clients and :5380 for the dashboard + REST API. README covers prerequisites, deploy, verification, and the ODoH privacy boundary (relay sees IP, target sees query, neither sees both). Advertised alongside packaging/relay/ in the main README Docker section.
Numa ODoH Client — Docker deploy
Single-container deploy that runs Numa as an ODoH (RFC 9230) client: every DNS query routes through an independent relay + target so neither operator sees both your IP and your question. See the ODoH integration doc for the full protocol and privacy trade-offs.
Prerequisites
- Docker + Docker Compose v2.
- Port 53 (UDP+TCP) free on the host — Numa listens there for DNS clients on your LAN.
Configure
The shipped numa.toml points at Numa's own public relay
(odoh-relay.numa.rs) paired with Cloudflare's ODoH target
(odoh.cloudflare-dns.com). That's two independent operators with
distinct eTLD+1s — the default configuration passes Numa's same-operator
check and works out of the box.
To use a different relay or target, edit numa.toml and adjust the URLs.
The relay and target must resolve to distinct operators or Numa
refuses to start.
Deploy
docker compose up -d
docker compose logs -f numa # watch startup
The first query fires the bootstrap resolver + ODoH config fetch; subsequent queries reuse the warm HTTP/2 connection.
Point your devices at it
Set each device's DNS server to the IP of the Docker host. For a LAN-wide rollout, set the DNS server in your router's DHCP config so every device picks it up automatically.
Verify a query landed on the ODoH path:
dig @<host-ip> example.com
curl http://<host-ip>:5380/stats | jq '.upstream_transport.odoh'
upstream_transport.odoh should increment on each query.
What this does NOT buy you
ODoH protects the path, not the content:
- The target (Cloudflare here) still sees the question. It just doesn't know it's you asking. If Cloudflare logs every ODoH query, the query is still visible — it's simply unattributed.
- The relay is a trusted party for availability. A malicious relay can drop or delay queries; it just can't read them.
- Traffic analysis defeats small relays. If you're the only client talking to a relay, timing alone re-identifies you. Shared, busy relays give better anonymity sets.
See the ODoH integration doc for more.
Relay operator?
If you'd rather run your own relay (same binary, different mode), see
../relay/ — that package spins up a public-facing relay
with Caddy + ACME in front of it.